Store app name and whether it is a launchable system app in metadata

this is helpful for restore, so the user can see app names when deciding which apps to restore
This commit is contained in:
Torsten Grote 2024-05-22 14:11:49 -03:00
parent 5a2f1187a8
commit 56d8d64261
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
11 changed files with 117 additions and 22 deletions

View file

@ -7,6 +7,7 @@
<w>ejectable</w>
<w>hasher</w>
<w>hkdf</w>
<w>launchable</w>
<w>restorable</w>
<w>seedvault</w>
<w>snowden</w>

View file

@ -26,7 +26,7 @@ data class BackupMetadata(
internal var d2dBackup: Boolean = false,
internal val packageMetadataMap: PackageMetadataMap = PackageMetadataMap(),
) {
val size: Long?
val size: Long
get() = packageMetadataMap.values.sumOf { m ->
(m.size ?: 0L) + (m.splits?.sumOf { it.size ?: 0L } ?: 0L)
}
@ -85,7 +85,9 @@ data class PackageMetadata(
internal var state: PackageState = UNKNOWN_ERROR,
internal var backupType: BackupType? = null,
internal var size: Long? = null,
internal var name: CharSequence? = null,
internal val system: Boolean = false,
internal val isLaunchableSystemApp: Boolean = false,
internal val version: Long? = null,
internal val installer: String? = null,
internal val splits: List<ApkSplit>? = null,
@ -110,7 +112,9 @@ internal const val JSON_PACKAGE_TIME = "time"
internal const val JSON_PACKAGE_BACKUP_TYPE = "backupType"
internal const val JSON_PACKAGE_STATE = "state"
internal const val JSON_PACKAGE_SIZE = "size"
internal const val JSON_PACKAGE_APP_NAME = "name"
internal const val JSON_PACKAGE_SYSTEM = "system"
internal const val JSON_PACKAGE_SYSTEM_LAUNCHER = "systemLauncher"
internal const val JSON_PACKAGE_VERSION = "version"
internal const val JSON_PACKAGE_INSTALLER = "installer"
internal const val JSON_PACKAGE_SPLITS = "splits"

View file

@ -23,6 +23,7 @@ import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.transport.backup.isSystemApp
import java.io.FileNotFoundException
import java.io.IOException
@ -41,6 +42,7 @@ internal class MetadataManager(
private val crypto: Crypto,
private val metadataWriter: MetadataWriter,
private val metadataReader: MetadataReader,
private val packageService: PackageService,
private val settingsManager: SettingsManager,
) {
@ -63,7 +65,11 @@ internal class MetadataManager(
return field
}
val backupSize: Long? get() = metadata.size
val backupSize: Long get() = metadata.size
private val launchableSystemApps by lazy {
packageService.launchableSystemApps.map { it.activityInfo.packageName }.toSet()
}
/**
* Call this when initializing a new device.
@ -111,8 +117,11 @@ internal class MetadataManager(
val oldPackageMetadata = metadata.packageMetadataMap[packageName]
?: PackageMetadata()
modifyCachedMetadata {
val isSystemApp = packageInfo.isSystemApp()
metadata.packageMetadataMap[packageName] = oldPackageMetadata.copy(
system = packageInfo.isSystemApp(),
name = packageInfo.applicationInfo?.loadLabel(context.packageManager),
system = isSystemApp,
isLaunchableSystemApp = isSystemApp && launchableSystemApps.contains(packageName),
version = packageMetadata.version,
installer = packageMetadata.installer,
splits = packageMetadata.splits,
@ -144,12 +153,16 @@ internal class MetadataManager(
metadata.time = now
metadata.d2dBackup = settingsManager.d2dBackupsEnabled()
metadata.packageMetadataMap.getOrPut(packageName) {
val isSystemApp = packageInfo.isSystemApp()
PackageMetadata(
time = now,
state = APK_AND_DATA,
backupType = type,
size = size,
system = packageInfo.isSystemApp(),
name = packageInfo.applicationInfo?.loadLabel(context.packageManager),
system = isSystemApp,
isLaunchableSystemApp = isSystemApp &&
launchableSystemApps.contains(packageName),
)
}.apply {
time = now
@ -157,6 +170,10 @@ internal class MetadataManager(
backupType = type
// don't override a previous K/V size, if there were no K/V changes
if (size != null) this.size = size
// update name, if none was set, yet (can happen while migrating to storing names)
if (this.name == null) {
this.name = packageInfo.applicationInfo?.loadLabel(context.packageManager)
}
}
}
}
@ -178,11 +195,15 @@ internal class MetadataManager(
check(packageState != APK_AND_DATA) { "Backup Error with non-error package state." }
modifyMetadata(metadataOutputStream) {
metadata.packageMetadataMap.getOrPut(packageInfo.packageName) {
val isSystemApp = packageInfo.isSystemApp()
PackageMetadata(
time = 0L,
state = packageState,
backupType = backupType,
system = packageInfo.isSystemApp()
name = packageInfo.applicationInfo?.loadLabel(context.packageManager),
system = isSystemApp,
isLaunchableSystemApp = isSystemApp &&
launchableSystemApps.contains(packageInfo.packageName),
)
}.state = packageState
}
@ -201,12 +222,22 @@ internal class MetadataManager(
packageState: PackageState,
) = modifyCachedMetadata {
metadata.packageMetadataMap.getOrPut(packageInfo.packageName) {
val isSystemApp = packageInfo.isSystemApp()
PackageMetadata(
time = 0L,
state = packageState,
system = packageInfo.isSystemApp(),
name = packageInfo.applicationInfo?.loadLabel(context.packageManager),
system = isSystemApp,
isLaunchableSystemApp = isSystemApp &&
launchableSystemApps.contains(packageInfo.packageName),
)
}.state = packageState
}.apply {
state = packageState
// update name, if none was set, yet (can happen while migrating to storing names)
if (this.name == null) {
this.name = packageInfo.applicationInfo?.loadLabel(context.packageManager)
}
}
}
/**

View file

@ -9,7 +9,7 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val metadataModule = module {
single { MetadataManager(androidContext(), get(), get(), get(), get(), get()) }
single { MetadataManager(androidContext(), get(), get(), get(), get(), get(), get()) }
single<MetadataWriter> { MetadataWriterImpl(get()) }
single<MetadataReader> { MetadataReaderImpl(get()) }
}

View file

@ -126,6 +126,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
else -> null
}
val pSize = p.optLong(JSON_PACKAGE_SIZE, -1L)
val pName = p.optString(JSON_PACKAGE_APP_NAME)
val pSystem = p.optBoolean(JSON_PACKAGE_SYSTEM, false)
val pVersion = p.optLong(JSON_PACKAGE_VERSION, 0L)
val pInstaller = p.optString(JSON_PACKAGE_INSTALLER)
@ -143,7 +144,9 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
state = pState,
backupType = pBackupType,
size = if (pSize < 0L) null else pSize,
name = if (pName == "") null else pName,
system = pSystem,
isLaunchableSystemApp = p.optBoolean(JSON_PACKAGE_SYSTEM_LAUNCHER, false),
version = if (pVersion == 0L) null else pVersion,
installer = if (pInstaller == "") null else pInstaller,
splits = getSplits(p),

View file

@ -57,8 +57,14 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
if (packageMetadata.size != null) {
put(JSON_PACKAGE_SIZE, packageMetadata.size)
}
if (packageMetadata.name != null) {
put(JSON_PACKAGE_APP_NAME, packageMetadata.name)
}
if (packageMetadata.system) {
put(JSON_PACKAGE_SYSTEM, packageMetadata.system)
put(JSON_PACKAGE_SYSTEM, true)
}
if (packageMetadata.isLaunchableSystemApp) {
put(JSON_PACKAGE_SYSTEM_LAUNCHER, true)
}
packageMetadata.version?.let { put(JSON_PACKAGE_VERSION, it) }
packageMetadata.installer?.let { put(JSON_PACKAGE_INSTALLER, it) }

View file

@ -7,11 +7,7 @@ package com.stevesoltys.seedvault.settings
import android.annotation.StringRes
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_MAIN
import android.content.Intent.CATEGORY_LAUNCHER
import android.content.pm.PackageManager
import android.content.pm.PackageManager.MATCH_SYSTEM_ONLY
import android.graphics.drawable.Drawable
import android.util.Log
import androidx.annotation.WorkerThread
@ -84,10 +80,6 @@ internal class AppListRetriever(
Pair(PACKAGE_NAME_CALL_LOG, R.string.backup_call_log),
Pair(PACKAGE_NAME_CONTACTS, R.string.backup_contacts)
)
// filter intent for apps with a launcher activity
val i = Intent(ACTION_MAIN).apply {
addCategory(CATEGORY_LAUNCHER)
}
return specialPackages.map { (packageName, stringId) ->
val metadata = metadataManager.getPackageMetadata(packageName)
val status = if (packageName == PACKAGE_NAME_CONTACTS && metadata?.state == null) {
@ -105,7 +97,7 @@ internal class AppListRetriever(
status = status,
isSpecial = true
)
} + context.packageManager.queryIntentActivities(i, MATCH_SYSTEM_ONLY).map {
} + packageService.launchableSystemApps.map {
val packageName = it.activityInfo.packageName
val metadata = metadataManager.getPackageMetadata(packageName)
AppStatus(

View file

@ -7,6 +7,9 @@ package com.stevesoltys.seedvault.transport.backup
import android.app.backup.IBackupManager
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_MAIN
import android.content.Intent.CATEGORY_LAUNCHER
import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
import android.content.pm.ApplicationInfo.FLAG_STOPPED
import android.content.pm.ApplicationInfo.FLAG_SYSTEM
@ -16,6 +19,8 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_INSTRUMENTATION
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
import android.content.pm.PackageManager.MATCH_SYSTEM_ONLY
import android.content.pm.ResolveInfo
import android.os.RemoteException
import android.os.UserHandle
import android.util.Log
@ -147,6 +152,16 @@ internal class PackageService(
}
}
val launchableSystemApps: List<ResolveInfo>
@WorkerThread
get() {
// filter intent for apps with a launcher activity
val i = Intent(ACTION_MAIN).apply {
addCategory(CATEGORY_LAUNCHER)
}
return packageManager.queryIntentActivities(i, MATCH_SYSTEM_ONLY)
}
fun getVersionName(packageName: String): String? = try {
packageManager.getPackageInfo(packageName, 0).versionName
} catch (e: PackageManager.NameNotFoundException) {

View file

@ -16,8 +16,10 @@ import com.stevesoltys.seedvault.metadata.metadataModule
import com.stevesoltys.seedvault.plugins.saf.storagePluginModuleSaf
import com.stevesoltys.seedvault.restore.install.installModule
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.transport.backup.backupModule
import com.stevesoltys.seedvault.transport.restore.restoreModule
import io.mockk.mockk
import org.koin.android.ext.koin.androidContext
import org.koin.core.KoinApplication
import org.koin.core.context.startKoin
@ -33,9 +35,11 @@ class TestApp : App() {
single<KeyManager> { KeyManagerTestImpl() }
single<Crypto> { CryptoImpl(get(), get(), get()) }
}
private val packageService: PackageService = mockk()
private val appModule = module {
single { Clock() }
single { SettingsManager(this@TestApp) }
single<PackageService> { packageService }
}
override fun startKoin(): KoinApplication {

View file

@ -7,11 +7,13 @@ package com.stevesoltys.seedvault.metadata
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
import android.content.pm.ApplicationInfo.FLAG_SYSTEM
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.os.UserManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.stevesoltys.seedvault.Clock
@ -27,6 +29,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.PackageService
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
@ -64,6 +67,7 @@ class MetadataManagerTest {
private val crypto: Crypto = mockk()
private val metadataWriter: MetadataWriter = mockk()
private val metadataReader: MetadataReader = mockk()
private val packageService: PackageService = mockk()
private val settingsManager: SettingsManager = mockk()
private val manager = MetadataManager(
@ -72,9 +76,12 @@ class MetadataManagerTest {
crypto = crypto,
metadataWriter = metadataWriter,
metadataReader = metadataReader,
settingsManager = settingsManager
packageService = packageService,
settingsManager = settingsManager,
)
private val packageManager: PackageManager = mockk()
private val time = 42L
private val token = Random.nextLong()
private val packageName = getRandomString()
@ -162,6 +169,7 @@ class MetadataManagerTest {
signatures = listOf("sig")
)
every { context.packageManager } returns packageManager
expectReadFromCache()
expectModifyMetadata(initialMetadata)
@ -185,12 +193,23 @@ class MetadataManagerTest {
signatures = listOf("sig")
)
every { context.packageManager } returns packageManager
every { packageService.launchableSystemApps } returns listOf(
ResolveInfo().apply {
activityInfo = ActivityInfo().apply {
packageName = this@MetadataManagerTest.packageName
}
}
)
expectReadFromCache()
expectModifyMetadata(initialMetadata)
manager.onApkBackedUp(packageInfo, packageMetadata)
assertEquals(packageMetadata.copy(system = true), manager.getPackageMetadata(packageName))
assertEquals(
packageMetadata.copy(system = true, isLaunchableSystemApp = true),
manager.getPackageMetadata(packageName),
)
verify {
cacheInputStream.close()
@ -214,6 +233,7 @@ class MetadataManagerTest {
signatures = listOf("sig foo")
)
every { context.packageManager } returns packageManager
expectReadFromCache()
expectWriteToCache(initialMetadata)
@ -236,6 +256,7 @@ class MetadataManagerTest {
signatures = listOf("sig")
)
every { context.packageManager } returns packageManager
expectReadFromCache()
expectWriteToCache(initialMetadata)
val oldState = UNKNOWN_ERROR
@ -295,6 +316,7 @@ class MetadataManagerTest {
signatures = listOf("sig")
)
every { context.packageManager } returns packageManager
expectReadFromCache()
assertNull(manager.getPackageMetadata(packageName))
@ -330,6 +352,8 @@ class MetadataManagerTest {
val packageMetadata = PackageMetadata(time)
updatedMetadata.packageMetadataMap[packageName] = packageMetadata
every { context.packageManager } returns packageManager
every { packageService.launchableSystemApps } returns emptyList()
expectReadFromCache()
every { clock.time() } returns time
expectModifyMetadata(initialMetadata)
@ -342,6 +366,7 @@ class MetadataManagerTest {
backupType = BackupType.FULL,
size = size,
system = true,
isLaunchableSystemApp = false,
),
manager.getPackageMetadata(packageName)
)
@ -361,6 +386,7 @@ class MetadataManagerTest {
expectModifyMetadata(initialMetadata)
every { settingsManager.d2dBackupsEnabled() } returns true
every { context.packageManager } returns packageManager
manager.onPackageBackedUp(packageInfo, BackupType.FULL, 0L, storageOutputStream)
assertTrue(initialMetadata.d2dBackup)
@ -382,6 +408,7 @@ class MetadataManagerTest {
updatedMetadata.packageMetadataMap[packageName] =
PackageMetadata(updateTime, APK_AND_DATA, BackupType.KV, size)
every { context.packageManager } returns packageManager
expectReadFromCache()
every { clock.time() } returns updateTime
every { metadataWriter.write(updatedMetadata, storageOutputStream) } throws IOException()
@ -414,6 +441,7 @@ class MetadataManagerTest {
PackageMetadata(time, state = APK_AND_DATA)
expectReadFromCache()
every { context.packageManager } returns packageManager
every { clock.time() } returns time
expectModifyMetadata(updatedMetadata)
@ -437,6 +465,7 @@ class MetadataManagerTest {
val updatedMetadata = initialMetadata.copy()
updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(state = NOT_ALLOWED)
every { context.packageManager } returns packageManager
expectReadFromCache()
expectWriteToCache(updatedMetadata)
@ -454,6 +483,7 @@ class MetadataManagerTest {
updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(state = WAS_STOPPED)
initialMetadata.packageMetadataMap.remove(packageName)
every { context.packageManager } returns packageManager
expectReadFromCache()
expectWriteToCache(updatedMetadata)
@ -482,6 +512,7 @@ class MetadataManagerTest {
updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(state = WAS_STOPPED)
initialMetadata.packageMetadataMap.remove(packageName)
every { context.packageManager } returns packageManager
expectReadFromCache()
expectModifyMetadata(updatedMetadata)

View file

@ -63,6 +63,10 @@ internal class MetadataWriterDecoderTest {
time = Random.nextLong(),
state = APK_AND_DATA,
backupType = BackupType.FULL,
size = Random.nextLong(0, Long.MAX_VALUE),
name = getRandomString(),
system = Random.nextBoolean(),
isLaunchableSystemApp = Random.nextBoolean(),
version = Random.nextLong(),
installer = getRandomString(),
splits = listOf(
@ -94,6 +98,7 @@ internal class MetadataWriterDecoderTest {
time = Random.nextLong(),
state = QUOTA_EXCEEDED,
backupType = BackupType.FULL,
name = null,
size = Random.nextLong(0..Long.MAX_VALUE),
system = Random.nextBoolean(),
version = Random.nextLong(),
@ -108,6 +113,7 @@ internal class MetadataWriterDecoderTest {
state = NO_DATA,
backupType = BackupType.KV,
size = null,
name = getRandomString(),
system = Random.nextBoolean(),
version = Random.nextLong(),
installer = getRandomString(),
@ -121,6 +127,7 @@ internal class MetadataWriterDecoderTest {
state = NOT_ALLOWED,
size = 0,
system = Random.nextBoolean(),
isLaunchableSystemApp = Random.nextBoolean(),
version = Random.nextLong(),
installer = getRandomString(),
sha256 = getRandomString(),
@ -138,10 +145,11 @@ internal class MetadataWriterDecoderTest {
private fun getMetadata(
packageMetadata: HashMap<String, PackageMetadata> = HashMap(),
): BackupMetadata {
val version = Random.nextBytes(1)[0]
return BackupMetadata(
version = Random.nextBytes(1)[0],
version = version,
token = Random.nextLong(),
salt = getRandomBase64(32),
salt = if (version != 0.toByte()) getRandomBase64(32) else "",
time = Random.nextLong(),
androidVersion = Random.nextInt(),
androidIncremental = getRandomString(),