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: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"
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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.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) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue