diff --git a/.idea/dictionaries/user.xml b/.idea/dictionaries/user.xml index 2317866a..db7d2d2c 100644 --- a/.idea/dictionaries/user.xml +++ b/.idea/dictionaries/user.xml @@ -7,6 +7,7 @@ ejectable hasher hkdf + launchable restorable seedvault snowden diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt index c36e00c1..3f39150c 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt @@ -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? = 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" diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt index daf7ebb0..c9c6dc39 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt @@ -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) + } + } } /** diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt index b0a10173..b5eaee76 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt @@ -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 { MetadataWriterImpl(get()) } single { MetadataReaderImpl(get()) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt index 8f77bcc4..98ffa9cb 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt @@ -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), diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt index dcfdbe7c..49e3c348 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt @@ -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) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt index b03a27d0..bb268c79 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt @@ -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( diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt index 39109bf2..c1fb8618 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt @@ -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 + @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) { diff --git a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt index 160dee86..78e9333a 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt @@ -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 { KeyManagerTestImpl() } single { CryptoImpl(get(), get(), get()) } } + private val packageService: PackageService = mockk() private val appModule = module { single { Clock() } single { SettingsManager(this@TestApp) } + single { packageService } } override fun startKoin(): KoinApplication { diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt index 0b1ff816..f3d0ee49 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt @@ -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) diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt index 24aad1cb..b1b1cb99 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt @@ -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 = 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(),