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:label="BackupJobService"
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
android:name=".storage.StorageBackupService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:label="BackupService" />
<!-- Does restore as a foreground service -->
<!-- Does file restore as a foreground service -->
<service
android:name=".storage.StorageRestoreService"
android:exported="false"

View file

@ -12,6 +12,7 @@ import android.app.backup.IRestoreObserver
import android.app.backup.IRestoreSession
import android.app.backup.RestoreSet
import android.content.Context
import android.content.Intent
import android.os.RemoteException
import android.os.UserHandle
import android.util.Log
@ -60,6 +61,7 @@ internal class AppDataRestoreManager(
private var session: IRestoreSession? = null
private val monitor = BackupMonitor()
private val foregroundServiceIntent = Intent(context, RestoreService::class.java)
private val mRestoreProgress = MutableLiveData(
LinkedList<AppRestoreResult>().apply {
@ -120,6 +122,8 @@ internal class AppDataRestoreManager(
mRestoreBackupResult.postValue(
RestoreBackupResult(context.getString(R.string.restore_set_error))
)
} else {
context.startForegroundService(foregroundServiceIntent)
}
}
@ -208,6 +212,8 @@ internal class AppDataRestoreManager(
mRestoreProgress.postValue(list)
mRestoreBackupResult.postValue(result)
context.stopService(foregroundServiceIntent)
}
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.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,17 @@ internal class ApkRestore(
return
}
mInstallResult.value = InstallResult(packages)
val i = Intent(context, RestoreService::class.java)
val autoRestore = backupStateManager.isAutoRestoreEnabled
try {
context.startForegroundService(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) }
}

View file

@ -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 {

View file

@ -169,6 +169,9 @@
<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_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_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>

View file

@ -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.startForegroundService(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

View file

@ -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.startForegroundService(any())
} returns ComponentName(strictContext, "org.foo.bar.Class")
every { strictContext.stopService(any()) } returns true
}
@Test