Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't re-install apps that are already installed and disable auto-restore #719

Merged
merged 3 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions app/src/main/java/com/stevesoltys/seedvault/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import android.os.ServiceManager.getService
import android.os.StrictMode
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
import androidx.work.WorkManager
import com.google.android.material.color.DynamicColors
Expand Down Expand Up @@ -148,6 +147,7 @@ open class App : Application() {
private val metadataManager: MetadataManager by inject()
private val backupManager: IBackupManager by inject()
private val pluginManager: StoragePluginManager by inject()
private val backupStateManager: BackupStateManager by inject()

/**
* The responsibility for the current token was moved to the [SettingsManager]
Expand All @@ -168,7 +168,7 @@ open class App : Application() {
* Introduced in the first half of 2024 and can be removed after a suitable migration period.
*/
protected open fun migrateToOwnScheduling() {
if (!isFrameworkSchedulingEnabled()) { // already on own scheduling
if (!backupStateManager.isFrameworkSchedulingEnabled) { // already on own scheduling
// fix things for removable drive users who had a job scheduled here before
if (pluginManager.isOnRemovableDrive) AppBackupWorker.unschedule(applicationContext)
return
Expand All @@ -184,10 +184,6 @@ open class App : Application() {
}
}

private fun isFrameworkSchedulingEnabled(): Boolean = Settings.Secure.getInt(
contentResolver, Settings.Secure.BACKUP_SCHEDULING_ENABLED, 1
) == 1 // 1 means enabled which is the default

}

const val MAGIC_PACKAGE_MANAGER: String = PACKAGE_MANAGER_SENTINEL
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/com/stevesoltys/seedvault/BackupStateManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
package com.stevesoltys.seedvault

import android.content.Context
import android.provider.Settings
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
import android.provider.Settings.Secure.BACKUP_SCHEDULING_ENABLED
import android.util.Log
import androidx.work.WorkInfo.State.RUNNING
import androidx.work.WorkManager
Expand All @@ -22,6 +25,7 @@ class BackupStateManager(
) {

private val workManager = WorkManager.getInstance(context)
private val contentResolver = context.contentResolver

val isBackupRunning: Flow<Boolean> = combine(
flow = ConfigurableBackupTransportService.isRunning,
Expand All @@ -37,4 +41,10 @@ class BackupStateManager(
appBackupRunning || filesBackupRunning || workInfoState == RUNNING
}

val isAutoRestoreEnabled: Boolean
get() = Settings.Secure.getInt(contentResolver, BACKUP_AUTO_RESTORE, 1) == 1

val isFrameworkSchedulingEnabled: Boolean
get() = Settings.Secure.getInt(contentResolver, BACKUP_SCHEDULING_ENABLED, 1) == 1

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@

package com.stevesoltys.seedvault.restore.install

import android.app.backup.IBackupManager
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_SIGNATURES
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
import android.util.Log
import com.stevesoltys.seedvault.BackupStateManager
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.metadata.ApkSplit
Expand Down Expand Up @@ -38,6 +40,8 @@ private val TAG = ApkRestore::class.java.simpleName

internal class ApkRestore(
private val context: Context,
private val backupManager: IBackupManager,
private val backupStateManager: BackupStateManager,
private val pluginManager: StoragePluginManager,
@Suppress("Deprecation")
private val legacyStoragePlugin: LegacyStoragePlugin,
Expand Down Expand Up @@ -81,14 +85,33 @@ internal class ApkRestore(
return
}
mInstallResult.value = InstallResult(packages)
val autoRestore = backupStateManager.isAutoRestoreEnabled
try {
// 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)
}
mInstallResult.update { it.copy(isFinished = true) }
}

private suspend fun reInstallApps(
backup: RestorableBackup,
packages: List<Map.Entry<String, ApkInstallResult>>,
) {
// re-install individual packages and emit updates (start from last and work your way up)
for ((packageName, apkInstallResult) in packages.asIterable().reversed()) {
for ((packageName, apkInstallResult) in packages) {
try {
if (apkInstallResult.metadata.hasApk()) {
restore(backup, packageName, apkInstallResult.metadata)
} else {
if (isInstalled(packageName, apkInstallResult.metadata)) {
mInstallResult.update { result ->
result.update(packageName) { it.copy(state = SUCCEEDED) }
}
} else if (!apkInstallResult.metadata.hasApk()) { // no APK available for install
mInstallResult.update { it.fail(packageName) }
} else {
restore(backup, packageName, apkInstallResult.metadata)
}
} catch (e: IOException) {
Log.e(TAG, "Error re-installing APK for $packageName.", e)
Expand All @@ -104,7 +127,23 @@ internal class ApkRestore(
mInstallResult.update { it.fail(packageName) }
}
}
mInstallResult.update { it.copy(isFinished = true) }
}

@Throws(SecurityException::class)
private fun isInstalled(packageName: String, metadata: PackageMetadata): Boolean {
@Suppress("DEPRECATION") // GET_SIGNATURES is needed even though deprecated
val flags = GET_SIGNING_CERTIFICATES or GET_SIGNATURES
val packageInfo = try {
pm.getPackageInfo(packageName, flags)
} catch (e: PackageManager.NameNotFoundException) {
null
} ?: return false
val signatures = metadata.signatures
if (signatures != null && signatures != packageInfo.signingInfo.getSignatures()) {
// this will get caught and flag app as failed, could receive dedicated handling later
throw SecurityException("Signature mismatch for $packageName")
}
return packageInfo.longVersionCode >= (metadata.version ?: 0)
}

@Suppress("ThrowsCount")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ val installModule = module {
factory { DeviceInfo(androidContext()) }
factory { ApkSplitCompatibilityChecker(get()) }
factory {
ApkRestore(androidContext(), get(), get(), get(), get(), get()) {
ApkRestore(androidContext(), get(), get(), get(), get(), get(), get(), get()) {
androidContext().getSystemService(UserManager::class.java)!!.isAllowedToInstallApks()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import android.content.Intent
import android.os.Bundle
import android.os.PowerManager
import android.os.RemoteException
import android.provider.Settings
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
Expand All @@ -25,6 +23,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.TwoStatePreference
import androidx.work.WorkInfo
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.stevesoltys.seedvault.BackupStateManager
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.plugins.StoragePluginManager
Expand All @@ -41,6 +40,7 @@ class SettingsFragment : PreferenceFragmentCompat() {

private val viewModel: SettingsViewModel by sharedViewModel()
private val storagePluginManager: StoragePluginManager by inject()
private val backupStateManager: BackupStateManager by inject()
private val backupManager: IBackupManager by inject()

private lateinit var backup: TwoStatePreference
Expand Down Expand Up @@ -251,7 +251,7 @@ class SettingsFragment : PreferenceFragmentCompat() {

private fun setAutoRestoreState() {
activity?.contentResolver?.let {
autoRestore.isChecked = Settings.Secure.getInt(it, BACKUP_AUTO_RESTORE, 1) == 1
autoRestore.isChecked = backupStateManager.isAutoRestoreEnabled
}
val storage = this.storageProperties
if (storage?.isUsb == true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@

package com.stevesoltys.seedvault.restore.install

import android.app.backup.IBackupManager
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.PackageManager.NameNotFoundException
import android.content.pm.Signature
import android.graphics.drawable.Drawable
import android.util.PackageUtils
import app.cash.turbine.test
import com.stevesoltys.seedvault.BackupStateManager
import com.stevesoltys.seedvault.assertReadEquals
import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.ApkSplit
Expand Down Expand Up @@ -56,6 +59,8 @@ internal class ApkBackupRestoreTest : TransportTest() {
}

private val storagePluginManager: StoragePluginManager = mockk()
private val backupManager: IBackupManager = mockk()
private val backupStateManager: BackupStateManager = mockk()

@Suppress("Deprecation")
private val legacyStoragePlugin: LegacyStoragePlugin = mockk()
Expand All @@ -67,6 +72,8 @@ internal class ApkBackupRestoreTest : TransportTest() {
private val apkBackup = ApkBackup(pm, crypto, settingsManager, metadataManager)
private val apkRestore: ApkRestore = ApkRestore(
context = strictContext,
backupManager = backupManager,
backupStateManager = backupStateManager,
pluginManager = storagePluginManager,
legacyStoragePlugin = legacyStoragePlugin,
crypto = crypto,
Expand Down Expand Up @@ -145,6 +152,8 @@ internal class ApkBackupRestoreTest : TransportTest() {
val cacheFiles = slot<List<File>>()

every { installRestriction.isAllowedToInstallApks() } returns true
every { backupStateManager.isAutoRestoreEnabled } returns false
every { pm.getPackageInfo(packageName, any<Int>()) } throws NameNotFoundException()
every { strictContext.cacheDir } returns tmpFile
every { crypto.getNameForApk(salt, packageName, "") } returns name
coEvery { storagePlugin.getInputStream(token, name) } returns inputStream
Expand Down
Loading
Loading