Start a foreground service during restore
so the system won't kill us, even if the user navigates away.
This commit is contained in:
parent
bebb9005da
commit
639947b87e
8 changed files with 107 additions and 5 deletions
|
@ -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"
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue