Start a foreground service during restore

so the system won't kill us, even if the user navigates away.
This commit is contained in:
Torsten Grote 2024-08-20 17:37:02 -03:00
parent bebb9005da
commit 639947b87e
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
8 changed files with 107 additions and 5 deletions

View file

@ -177,13 +177,19 @@
android:exported="false" android:exported="false"
android:label="BackupJobService" android:label="BackupJobService"
android:permission="android.permission.BIND_JOB_SERVICE" /> android:permission="android.permission.BIND_JOB_SERVICE" />
<!-- Does the actual backup work as a foreground service --> <!-- Does app restore as a foreground service -->
<service
android:name=".restore.RestoreService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:label="RestoreService" />
<!-- Does the actual file backup work as a foreground service -->
<service <service
android:name=".storage.StorageBackupService" android:name=".storage.StorageBackupService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" android:foregroundServiceType="dataSync"
android:label="BackupService" /> android:label="BackupService" />
<!-- Does restore as a foreground service --> <!-- Does file restore as a foreground service -->
<service <service
android:name=".storage.StorageRestoreService" android:name=".storage.StorageRestoreService"
android:exported="false" android:exported="false"

View file

@ -12,6 +12,7 @@ import android.app.backup.IRestoreObserver
import android.app.backup.IRestoreSession import android.app.backup.IRestoreSession
import android.app.backup.RestoreSet import android.app.backup.RestoreSet
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.RemoteException import android.os.RemoteException
import android.os.UserHandle import android.os.UserHandle
import android.util.Log import android.util.Log
@ -60,6 +61,7 @@ internal class AppDataRestoreManager(
private var session: IRestoreSession? = null private var session: IRestoreSession? = null
private val monitor = BackupMonitor() private val monitor = BackupMonitor()
private val foregroundServiceIntent = Intent(context, RestoreService::class.java)
private val mRestoreProgress = MutableLiveData( private val mRestoreProgress = MutableLiveData(
LinkedList<AppRestoreResult>().apply { LinkedList<AppRestoreResult>().apply {
@ -120,6 +122,8 @@ internal class AppDataRestoreManager(
mRestoreBackupResult.postValue( mRestoreBackupResult.postValue(
RestoreBackupResult(context.getString(R.string.restore_set_error)) RestoreBackupResult(context.getString(R.string.restore_set_error))
) )
} else {
context.startForegroundService(foregroundServiceIntent)
} }
} }
@ -208,6 +212,8 @@ internal class AppDataRestoreManager(
mRestoreProgress.postValue(list) mRestoreProgress.postValue(list)
mRestoreBackupResult.postValue(result) mRestoreBackupResult.postValue(result)
context.stopService(foregroundServiceIntent)
} }
fun closeSession() { fun closeSession() {

View file

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

View file

@ -7,6 +7,7 @@ package com.stevesoltys.seedvault.restore.install
import android.app.backup.IBackupManager import android.app.backup.IBackupManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_SIGNATURES import android.content.pm.PackageManager.GET_SIGNATURES
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES 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.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.restore.RestorableBackup 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
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
@ -85,14 +87,17 @@ internal class ApkRestore(
return return
} }
mInstallResult.value = InstallResult(packages) mInstallResult.value = InstallResult(packages)
val i = Intent(context, RestoreService::class.java)
val autoRestore = backupStateManager.isAutoRestoreEnabled val autoRestore = backupStateManager.isAutoRestoreEnabled
try { try {
context.startForegroundService(i)
// disable auto-restore before installing apps, if it was enabled before // disable auto-restore before installing apps, if it was enabled before
if (autoRestore) backupManager.setAutoRestore(false) if (autoRestore) backupManager.setAutoRestore(false)
reInstallApps(backup, packages.asIterable().reversed()) reInstallApps(backup, packages.asIterable().reversed())
} finally { } finally {
// re-enable auto-restore, if it was enabled before // re-enable auto-restore, if it was enabled before
if (autoRestore) backupManager.setAutoRestore(true) if (autoRestore) backupManager.setAutoRestore(true)
context.stopService(i)
} }
mInstallResult.update { it.copy(isFinished = true) } mInstallResult.update { it.copy(isFinished = true) }
} }

View file

@ -38,14 +38,16 @@ import kotlin.math.min
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver" private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
private const val CHANNEL_ID_SUCCESS = "NotificationBackupSuccess" private const val CHANNEL_ID_SUCCESS = "NotificationBackupSuccess"
private const val CHANNEL_ID_ERROR = "NotificationError" private const val CHANNEL_ID_ERROR = "NotificationError"
private const val CHANNEL_ID_RESTORE = "NotificationRestore"
private const val CHANNEL_ID_RESTORE_ERROR = "NotificationRestoreError" private const val CHANNEL_ID_RESTORE_ERROR = "NotificationRestoreError"
internal const val NOTIFICATION_ID_OBSERVER = 1 internal const val NOTIFICATION_ID_OBSERVER = 1
private const val NOTIFICATION_ID_SUCCESS = 2 private const val NOTIFICATION_ID_SUCCESS = 2
private const val NOTIFICATION_ID_ERROR = 3 private const val NOTIFICATION_ID_ERROR = 3
private const val NOTIFICATION_ID_SPACE_ERROR = 4 private const val NOTIFICATION_ID_SPACE_ERROR = 4
private const val NOTIFICATION_ID_RESTORE_ERROR = 5 internal const val NOTIFICATION_ID_RESTORE = 5
private const val NOTIFICATION_ID_BACKGROUND = 6 private const val NOTIFICATION_ID_RESTORE_ERROR = 6
private const val NOTIFICATION_ID_NO_MAIN_KEY_ERROR = 7 private const val NOTIFICATION_ID_BACKGROUND = 7
private const val NOTIFICATION_ID_NO_MAIN_KEY_ERROR = 8
private val TAG = BackupNotificationManager::class.java.simpleName private val TAG = BackupNotificationManager::class.java.simpleName
@ -55,6 +57,7 @@ internal class BackupNotificationManager(private val context: Context) {
createNotificationChannel(getObserverChannel()) createNotificationChannel(getObserverChannel())
createNotificationChannel(getSuccessChannel()) createNotificationChannel(getSuccessChannel())
createNotificationChannel(getErrorChannel()) createNotificationChannel(getErrorChannel())
createNotificationChannel(getRestoreChannel())
createNotificationChannel(getRestoreErrorChannel()) createNotificationChannel(getRestoreErrorChannel())
} }
@ -77,6 +80,11 @@ internal class BackupNotificationManager(private val context: Context) {
return NotificationChannel(CHANNEL_ID_ERROR, title, IMPORTANCE_DEFAULT) 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 { private fun getRestoreErrorChannel(): NotificationChannel {
val title = context.getString(R.string.notification_restore_error_channel_title) val title = context.getString(R.string.notification_restore_error_channel_title)
return NotificationChannel(CHANNEL_ID_RESTORE_ERROR, title, IMPORTANCE_HIGH) 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) 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") @SuppressLint("RestrictedApi")
fun onRemovableStorageNotAvailableForRestore(packageName: String, storageName: String) { fun onRemovableStorageNotAvailableForRestore(packageName: String, storageName: String) {
val appName = try { val appName = try {

View file

@ -169,6 +169,9 @@
<string name="notification_space_error_title">Insufficient backup space</string> <string name="notification_space_error_title">Insufficient backup space</string>
<string name="notification_space_error_text">Your backup location is running out of space. Free up space, so backups can run.</string> <string name="notification_space_error_text">Your backup location is running out of space. Free up space, so backups can run.</string>
<string name="notification_restore_channel_title">Restore notification</string>
<string name="notification_restore_title">Restore running</string>
<string name="notification_restore_error_channel_title">Auto restore flash drive error</string> <string name="notification_restore_error_channel_title">Auto restore flash drive error</string>
<string name="notification_restore_error_title">Could not restore data for %1$s</string> <string name="notification_restore_error_title">Could not restore data for %1$s</string>
<string name="notification_restore_error_text">Plug in your %1$s before installing the app to restore its data from backup.</string> <string name="notification_restore_error_text">Plug in your %1$s before installing the app to restore its data from backup.</string>

View file

@ -6,6 +6,7 @@
package com.stevesoltys.seedvault.restore.install package com.stevesoltys.seedvault.restore.install
import android.app.backup.IBackupManager import android.app.backup.IBackupManager
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.PackageManager.NameNotFoundException
@ -127,6 +128,13 @@ internal class ApkBackupRestoreTest : TransportTest() {
writeBytes(splitBytes) writeBytes(splitBytes)
}.absolutePath) }.absolutePath)
// related to starting/stopping service
every { strictContext.packageName } returns "org.foo.bar"
every {
strictContext.startForegroundService(any())
} returns ComponentName(strictContext, "org.foo.bar.Class")
every { strictContext.stopService(any()) } returns true
every { settingsManager.isBackupEnabled(any()) } returns true every { settingsManager.isBackupEnabled(any()) } returns true
every { settingsManager.backupApks() } returns true every { settingsManager.backupApks() } returns true
every { sigInfo.hasMultipleSigners() } returns false every { sigInfo.hasMultipleSigners() } returns false

View file

@ -6,6 +6,7 @@
package com.stevesoltys.seedvault.restore.install package com.stevesoltys.seedvault.restore.install
import android.app.backup.IBackupManager import android.app.backup.IBackupManager
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo.FLAG_INSTALLED import android.content.pm.ApplicationInfo.FLAG_INSTALLED
import android.content.pm.ApplicationInfo.FLAG_SYSTEM import android.content.pm.ApplicationInfo.FLAG_SYSTEM
@ -107,6 +108,13 @@ internal class ApkRestoreTest : TransportTest() {
packageInfo.signingInfo = mockk(relaxed = true) packageInfo.signingInfo = mockk(relaxed = true)
every { storagePluginManager.appPlugin } returns storagePlugin every { storagePluginManager.appPlugin } returns storagePlugin
// related to starting/stopping service
every { strictContext.packageName } returns "org.foo.bar"
every {
strictContext.startForegroundService(any())
} returns ComponentName(strictContext, "org.foo.bar.Class")
every { strictContext.stopService(any()) } returns true
} }
@Test @Test