From 64d0c87013464d89dc589cc2b42a0c2df85f9cc2 Mon Sep 17 00:00:00 2001 From: Steve Soltys Date: Sat, 27 Jan 2024 21:27:59 +0000 Subject: [PATCH 01/76] Add support for RoundSync as a storage provider --- app/development/scripts/install_app.sh | 2 + app/src/debug/res/values/config.xml | 1 + .../seedvault/ui/storage/SafStorageOptions.kt | 53 ++++++++++++++++++- .../ui/storage/StorageRootFetcher.kt | 1 + .../ui/storage/StorageRootResolver.kt | 4 ++ app/src/main/res/drawable/round_sync.xml | 5 ++ .../res/drawable/round_sync_foreground.xml | 18 +++++++ app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/config.xml | 1 + app/src/main/res/values/strings.xml | 1 + 10 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/round_sync.xml create mode 100644 app/src/main/res/drawable/round_sync_foreground.xml diff --git a/app/development/scripts/install_app.sh b/app/development/scripts/install_app.sh index 930c7a1f3..93180f4ab 100755 --- a/app/development/scripts/install_app.sh +++ b/app/development/scripts/install_app.sh @@ -30,6 +30,8 @@ $ADB push $ROOT_PROJECT_DIR/app/build/outputs/apk/release/app-release.apk /syste echo "Installing Seedvault permissions..." $ADB push $ROOT_PROJECT_DIR/permissions_com.stevesoltys.seedvault.xml /system/etc/permissions/privapp-permissions-seedvault.xml $ADB push $ROOT_PROJECT_DIR/allowlist_com.stevesoltys.seedvault.xml /system/etc/sysconfig/allowlist-seedvault.xml +$ADB shell am force-stop com.stevesoltys.seedvault +$ADB shell am broadcast -a android.intent.action.BOOT_COMPLETED echo "Setting Seedvault transport..." $ADB shell bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport diff --git a/app/src/debug/res/values/config.xml b/app/src/debug/res/values/config.xml index 50bc22f89..a814cb633 100644 --- a/app/src/debug/res/values/config.xml +++ b/app/src/debug/res/values/config.xml @@ -7,5 +7,6 @@ org.nextcloud.documents org.nextcloud.beta.documents at.bitfire.davdroid.webdav + de.felixnuesse.extract.vcp diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/SafStorageOptions.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/SafStorageOptions.kt index f3e9ba6b7..6d86e8608 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/SafStorageOptions.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/SafStorageOptions.kt @@ -5,6 +5,9 @@ import android.content.Intent import android.content.Intent.ACTION_VIEW import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.net.Uri +import android.provider.DocumentsContract +import android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME +import android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption import com.stevesoltys.seedvault.ui.storage.StorageRootResolver.getIcon @@ -30,6 +33,7 @@ internal class SafStorageOptions( checkOrAddUsbRoot(roots) checkOrAddDavX5Root(roots) checkOrAddNextCloudRoot(roots) + checkOrAddRoundSyncRoots(roots) } private fun checkOrAddUsbRoot(roots: ArrayList) { @@ -50,6 +54,49 @@ internal class SafStorageOptions( roots.add(root) } + /** + * Add a storage root for each child directory at the RoundSync root, if it exists. + */ + private fun checkOrAddRoundSyncRoots(roots: ArrayList) { + + val roundSyncRoot = roots.firstOrNull { + it.authority == AUTHORITY_ROUND_SYNC + } ?: return + + roots.remove(roundSyncRoot) + + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( + roundSyncRoot.uri, roundSyncRoot.documentId + ) + val projection = arrayOf(COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID) + val cursor = context.contentResolver.query(childrenUri, projection, null, null, null) + + cursor?.use { + val nameIndex = cursor.getColumnIndex(COLUMN_DISPLAY_NAME) + val documentIdIndex = cursor.getColumnIndex(COLUMN_DOCUMENT_ID) + + while (cursor.moveToNext()) { + val name = cursor.getString(nameIndex) + val documentId = cursor.getString(documentIdIndex) + + val childRoot = SafOption( + authority = AUTHORITY_ROUND_SYNC, + rootId = name, + documentId = documentId, + icon = getIcon(context, AUTHORITY_ROUND_SYNC, name, 0), + title = name, + summary = context.getString(R.string.storage_round_sync_summary_prefix) + name, + availableBytes = null, + isUsb = false, + requiresNetwork = true, + enabled = true + ) + + roots.add(childRoot) + } + } + } + /** * This adds a fake Dav X5 entry if no real one was found. * @@ -136,8 +183,10 @@ internal class SafStorageOptions( rootId = "fake", documentId = "fake", icon = getIcon(context, AUTHORITY_NEXTCLOUD, "fake", 0), - title = context.getString(R.string.storage_not_recommended, - context.getString(R.string.storage_fake_nextcloud_title)), + title = context.getString( + R.string.storage_not_recommended, + context.getString(R.string.storage_fake_nextcloud_title) + ), summary = context.getString(summaryRes), availableBytes = null, isUsb = false, diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootFetcher.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootFetcher.kt index 96c27d0ef..1357f2999 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootFetcher.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootFetcher.kt @@ -24,6 +24,7 @@ const val ROOT_ID_HOME = "home" const val AUTHORITY_DOWNLOADS = "com.android.providers.downloads.documents" const val AUTHORITY_NEXTCLOUD = "org.nextcloud.documents" const val AUTHORITY_DAVX5 = "at.bitfire.davdroid.webdav" +const val AUTHORITY_ROUND_SYNC = "de.felixnuesse.extract.vcp" internal interface RemovableStorageListener { fun onStorageChanged() diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootResolver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootResolver.kt index 5a48e2e77..9b9c95bb8 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootResolver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootResolver.kt @@ -141,6 +141,10 @@ internal object StorageRootResolver { context.getDrawable(R.drawable.davx5) } + authority == AUTHORITY_ROUND_SYNC -> { + context.getDrawable(R.drawable.round_sync) + } + else -> null } } diff --git a/app/src/main/res/drawable/round_sync.xml b/app/src/main/res/drawable/round_sync.xml new file mode 100644 index 000000000..ae9c19466 --- /dev/null +++ b/app/src/main/res/drawable/round_sync.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_sync_foreground.xml b/app/src/main/res/drawable/round_sync_foreground.xml new file mode 100644 index 000000000..0fdb74675 --- /dev/null +++ b/app/src/main/res/drawable/round_sync_foreground.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index bab25339e..41257d635 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -24,4 +24,5 @@ #558B2F #F9A825 #7cb342 + #4bae4f diff --git a/app/src/main/res/values/config.xml b/app/src/main/res/values/config.xml index fe2f35e1a..af80093aa 100644 --- a/app/src/main/res/values/config.xml +++ b/app/src/main/res/values/config.xml @@ -18,6 +18,7 @@ com.android.externalstorage.documents org.nextcloud.documents at.bitfire.davdroid.webdav + de.felixnuesse.extract.vcp Backup notification + APK backup notification Success notification Backup running + Backing up APK of %s + Saving list of apps we can not back up. Backup already in progress Backup not enabled 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 d1277132e..f3dbb0167 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt @@ -28,10 +28,12 @@ import io.mockk.verify import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Before import org.junit.Test +import org.junit.jupiter.api.assertThrows import org.junit.runner.RunWith import org.koin.core.context.stopKoin import org.robolectric.annotation.Config @@ -121,7 +123,7 @@ class MetadataManagerTest { expectReadFromCache() expectModifyMetadata(initialMetadata) - manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream) + manager.onApkBackedUp(packageInfo, packageMetadata) assertEquals(packageMetadata, manager.getPackageMetadata(packageName)) @@ -144,7 +146,7 @@ class MetadataManagerTest { expectReadFromCache() expectModifyMetadata(initialMetadata) - manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream) + manager.onApkBackedUp(packageInfo, packageMetadata) assertEquals(packageMetadata.copy(system = true), manager.getPackageMetadata(packageName)) @@ -171,9 +173,9 @@ class MetadataManagerTest { ) expectReadFromCache() - expectModifyMetadata(initialMetadata) + expectWriteToCache(initialMetadata) - manager.onApkBackedUp(packageInfo, updatedPackageMetadata, storageOutputStream) + manager.onApkBackedUp(packageInfo, updatedPackageMetadata) assertEquals(updatedPackageMetadata, manager.getPackageMetadata(packageName)) @@ -184,7 +186,7 @@ class MetadataManagerTest { } @Test - fun `test onApkBackedUp() limits state changes`() { + fun `test onApkBackedUp() does not change package state`() { var version = Random.nextLong(Long.MAX_VALUE) var packageMetadata = PackageMetadata( version = version, @@ -193,12 +195,12 @@ class MetadataManagerTest { ) expectReadFromCache() - expectModifyMetadata(initialMetadata) + expectWriteToCache(initialMetadata) val oldState = UNKNOWN_ERROR // state doesn't change for APK_AND_DATA packageMetadata = packageMetadata.copy(version = ++version, state = APK_AND_DATA) - manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream) + manager.onApkBackedUp(packageInfo, packageMetadata) assertEquals( packageMetadata.copy(state = oldState), manager.getPackageMetadata(packageName) @@ -206,7 +208,7 @@ class MetadataManagerTest { // state doesn't change for QUOTA_EXCEEDED packageMetadata = packageMetadata.copy(version = ++version, state = QUOTA_EXCEEDED) - manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream) + manager.onApkBackedUp(packageInfo, packageMetadata) assertEquals( packageMetadata.copy(state = oldState), manager.getPackageMetadata(packageName) @@ -214,25 +216,25 @@ class MetadataManagerTest { // state doesn't change for NO_DATA packageMetadata = packageMetadata.copy(version = ++version, state = NO_DATA) - manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream) + manager.onApkBackedUp(packageInfo, packageMetadata) assertEquals( packageMetadata.copy(state = oldState), manager.getPackageMetadata(packageName) ) - // state DOES change for NOT_ALLOWED + // state doesn't change for NOT_ALLOWED packageMetadata = packageMetadata.copy(version = ++version, state = NOT_ALLOWED) - manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream) + manager.onApkBackedUp(packageInfo, packageMetadata) assertEquals( - packageMetadata.copy(state = NOT_ALLOWED), + packageMetadata.copy(state = oldState), manager.getPackageMetadata(packageName) ) - // state DOES change for WAS_STOPPED + // state doesn't change for WAS_STOPPED packageMetadata = packageMetadata.copy(version = ++version, state = WAS_STOPPED) - manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream) + manager.onApkBackedUp(packageInfo, packageMetadata) assertEquals( - packageMetadata.copy(state = WAS_STOPPED), + packageMetadata.copy(state = oldState), manager.getPackageMetadata(packageName) ) @@ -242,6 +244,39 @@ class MetadataManagerTest { } } + @Test + fun `test onApkBackedUp() throws while writing local cache`() { + val packageMetadata = PackageMetadata( + time = 0L, + version = Random.nextLong(Long.MAX_VALUE), + installer = getRandomString(), + signatures = listOf("sig") + ) + + expectReadFromCache() + + assertNull(manager.getPackageMetadata(packageName)) + + every { metadataWriter.encode(initialMetadata) } returns encodedMetadata + every { + context.openFileOutput( + METADATA_CACHE_FILE, + MODE_PRIVATE + ) + } throws FileNotFoundException() + + assertThrows { + manager.onApkBackedUp(packageInfo, packageMetadata) + } + + // metadata change got reverted + assertNull(manager.getPackageMetadata(packageName)) + + verify { + cacheInputStream.close() + } + } + @Test fun `test onPackageBackedUp()`() { packageInfo.applicationInfo.flags = FLAG_SYSTEM @@ -317,10 +352,7 @@ class MetadataManagerTest { } assertEquals(0L, manager.getLastBackupTime()) // time was reverted - assertEquals( - initialMetadata.packageMetadataMap[packageName], - manager.getPackageMetadata(packageName) - ) + assertNull(manager.getPackageMetadata(packageName)) // no package metadata got added verify { cacheInputStream.close() } } @@ -358,6 +390,70 @@ class MetadataManagerTest { } } + @Test + fun `test onPackageDoesNotGetBackedUp() updates state`() { + val updatedMetadata = initialMetadata.copy() + updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(state = NOT_ALLOWED) + + expectReadFromCache() + expectWriteToCache(updatedMetadata) + + manager.onPackageDoesNotGetBackedUp(packageInfo, NOT_ALLOWED) + + assertEquals( + updatedMetadata.packageMetadataMap[packageName], + manager.getPackageMetadata(packageName), + ) + } + + @Test + fun `test onPackageDoesNotGetBackedUp() creates new state`() { + val updatedMetadata = initialMetadata.copy() + updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(state = WAS_STOPPED) + initialMetadata.packageMetadataMap.remove(packageName) + + expectReadFromCache() + expectWriteToCache(updatedMetadata) + + manager.onPackageDoesNotGetBackedUp(packageInfo, WAS_STOPPED) + + assertEquals( + updatedMetadata.packageMetadataMap[packageName], + manager.getPackageMetadata(packageName), + ) + } + + @Test + fun `test onPackageBackupError() updates state`() { + val updatedMetadata = initialMetadata.copy() + updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(state = NO_DATA) + + expectReadFromCache() + expectModifyMetadata(updatedMetadata) + + manager.onPackageBackupError(packageInfo, NO_DATA, storageOutputStream, BackupType.KV) + } + + @Test + fun `test onPackageBackupError() inserts new package`() { + val updatedMetadata = initialMetadata.copy() + updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(state = WAS_STOPPED) + initialMetadata.packageMetadataMap.remove(packageName) + + expectReadFromCache() + expectModifyMetadata(updatedMetadata) + + manager.onPackageBackupError(packageInfo, WAS_STOPPED, storageOutputStream) + } + + @Test + fun `test uploadMetadata() uploads`() { + expectReadFromCache() + every { metadataWriter.write(initialMetadata, storageOutputStream) } just Runs + + manager.uploadMetadata(storageOutputStream) + } + @Test fun `test getBackupToken() on first run`() { every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException() @@ -386,15 +482,7 @@ class MetadataManagerTest { private fun expectModifyMetadata(metadata: BackupMetadata) { every { metadataWriter.write(metadata, storageOutputStream) } just Runs - every { metadataWriter.encode(metadata) } returns encodedMetadata - every { - context.openFileOutput( - METADATA_CACHE_FILE, - MODE_PRIVATE - ) - } returns cacheOutputStream - every { cacheOutputStream.write(encodedMetadata) } just Runs - every { cacheOutputStream.close() } just Runs + expectWriteToCache(metadata) } private fun expectReadFromCache() { @@ -406,4 +494,16 @@ class MetadataManagerTest { every { cacheInputStream.close() } just Runs } + private fun expectWriteToCache(metadata: BackupMetadata) { + every { metadataWriter.encode(metadata) } returns encodedMetadata + every { + context.openFileOutput( + METADATA_CACHE_FILE, + MODE_PRIVATE + ) + } returns cacheOutputStream + every { cacheOutputStream.write(encodedMetadata) } just Runs + every { cacheOutputStream.close() } just Runs + } + } diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt index f712807b3..33a244aa1 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt @@ -10,12 +10,11 @@ import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.metadata.ApkSplit import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageMetadataMap -import com.stevesoltys.seedvault.metadata.PackageState import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.restore.RestorableBackup import com.stevesoltys.seedvault.transport.TransportTest -import com.stevesoltys.seedvault.transport.backup.ApkBackup +import com.stevesoltys.seedvault.worker.ApkBackup import io.mockk.coEvery import io.mockk.every import io.mockk.mockk @@ -121,7 +120,7 @@ internal class ApkBackupRestoreTest : TransportTest() { every { crypto.getNameForApk(salt, packageName, splitName) } returns suffixName every { storagePlugin.providerPackageName } returns storageProviderPackageName - apkBackup.backupApkIfNecessary(packageInfo, PackageState.APK_AND_DATA, outputStreamGetter) + apkBackup.backupApkIfNecessary(packageInfo, outputStreamGetter) assertArrayEquals(apkBytes, outputStream.toByteArray()) assertArrayEquals(splitBytes, splitOutputStream.toByteArray()) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt index 0ff406d26..94d3ea8c9 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt @@ -15,11 +15,9 @@ import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH import com.stevesoltys.seedvault.metadata.BackupType import com.stevesoltys.seedvault.metadata.MetadataReaderImpl import com.stevesoltys.seedvault.metadata.PackageMetadata -import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA -import com.stevesoltys.seedvault.transport.backup.ApkBackup import com.stevesoltys.seedvault.transport.backup.BackupCoordinator import com.stevesoltys.seedvault.transport.backup.FullBackup import com.stevesoltys.seedvault.transport.backup.InputFactory @@ -31,6 +29,7 @@ import com.stevesoltys.seedvault.transport.restore.KVRestore import com.stevesoltys.seedvault.transport.restore.OutputFactory import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import com.stevesoltys.seedvault.worker.ApkBackup import io.mockk.CapturingSlot import io.mockk.Runs import io.mockk.coEvery @@ -73,7 +72,6 @@ internal class CoordinatorIntegrationTest : TransportTest() { backupPlugin, kvBackup, fullBackup, - apkBackup, clock, packageService, metadataManager, @@ -138,13 +136,13 @@ internal class CoordinatorIntegrationTest : TransportTest() { appData2.size } coEvery { - apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) + apkBackup.backupApkIfNecessary(packageInfo, any()) } returns packageMetadata coEvery { backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream every { - metadataManager.onApkBackedUp(packageInfo, packageMetadata, metadataOutputStream) + metadataManager.onApkBackedUp(packageInfo, packageMetadata) } just Runs every { metadataManager.onPackageBackedUp( @@ -215,7 +213,7 @@ internal class CoordinatorIntegrationTest : TransportTest() { appData.copyInto(value.captured) // write the app data into the passed ByteArray appData.size } - coEvery { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns null + coEvery { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns null every { settingsManager.getToken() } returns token coEvery { backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA) @@ -279,25 +277,13 @@ internal class CoordinatorIntegrationTest : TransportTest() { coEvery { backupPlugin.getOutputStream(token, realName) } returns bOutputStream every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream every { settingsManager.isQuotaUnlimited() } returns false - coEvery { - apkBackup.backupApkIfNecessary( - packageInfo, - UNKNOWN_ERROR, - any() - ) - } returns packageMetadata + coEvery { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns packageMetadata every { settingsManager.getToken() } returns token every { metadataManager.salt } returns salt coEvery { backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream - every { - metadataManager.onApkBackedUp( - packageInfo, - packageMetadata, - metadataOutputStream - ) - } just Runs + every { metadataManager.onApkBackedUp(packageInfo, packageMetadata) } just Runs every { metadataManager.onPackageBackedUp( packageInfo = packageInfo, diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt index 30d2aa16b..a883aaa40 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt @@ -5,7 +5,6 @@ import android.app.backup.BackupTransport.TRANSPORT_NOT_INITIALIZED import android.app.backup.BackupTransport.TRANSPORT_OK import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED -import android.content.pm.ApplicationInfo.FLAG_STOPPED import android.content.pm.PackageInfo import android.net.Uri import android.os.ParcelFileDescriptor @@ -14,18 +13,15 @@ import com.stevesoltys.seedvault.coAssertThrows import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.metadata.BackupType import com.stevesoltys.seedvault.metadata.PackageMetadata -import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA 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.plugins.StoragePlugin import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA import com.stevesoltys.seedvault.settings.Storage import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import com.stevesoltys.seedvault.worker.ApkBackup import io.mockk.Runs import io.mockk.coEvery -import io.mockk.coVerify import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -36,7 +32,6 @@ import org.junit.jupiter.api.Test import java.io.IOException import java.io.OutputStream import kotlin.random.Random -import kotlin.random.nextLong @Suppress("BlockingMethodInNonBlockingContext") internal class BackupCoordinatorTest : BackupTest() { @@ -53,7 +48,6 @@ internal class BackupCoordinatorTest : BackupTest() { plugin, kv, full, - apkBackup, clock, packageService, metadataManager, @@ -157,16 +151,12 @@ internal class BackupCoordinatorTest : BackupTest() { val isFullBackup = Random.nextBoolean() val quota = Random.nextLong() - expectApkBackupAndMetadataWrite() if (isFullBackup) { every { full.getQuota() } returns quota } else { every { kv.getQuota() } returns quota } - every { metadataOutputStream.close() } just Runs assertEquals(quota, backup.getBackupQuota(packageInfo.packageName, isFullBackup)) - - verify { metadataOutputStream.close() } } @Test @@ -276,7 +266,7 @@ internal class BackupCoordinatorTest : BackupTest() { coEvery { full.performFullBackup(packageInfo, fileDescriptor, 0, token, salt) } returns TRANSPORT_OK - coEvery { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns null + coEvery { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns null assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0)) } @@ -380,180 +370,13 @@ internal class BackupCoordinatorTest : BackupTest() { @Test fun `not allowed apps get their APKs backed up after @pm@ backup`() = runBlocking { - val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER } - val notAllowedPackages = listOf( - PackageInfo().apply { packageName = "org.example.1" }, - PackageInfo().apply { - packageName = "org.example.2" - // the second package does not get backed up, because it is stopped - applicationInfo = mockk { - flags = FLAG_STOPPED - } - } - ) - val packageMetadata: PackageMetadata = mockk() - val size = Random.nextLong(1L..Long.MAX_VALUE) - - every { settingsManager.canDoBackupNow() } returns true - every { metadataManager.requiresInit } returns false - every { settingsManager.getToken() } returns token - every { metadataManager.salt } returns salt - // do actual @pm@ backup - coEvery { - kv.performBackup(packageInfo, fileDescriptor, 0, token, salt) - } returns TRANSPORT_OK - - assertEquals( - TRANSPORT_OK, - backup.performIncrementalBackup(packageInfo, fileDescriptor, 0) - ) - - // finish @pm@ backup - every { kv.hasState() } returns true - every { full.hasState() } returns false - every { kv.getCurrentPackage() } returns pmPackageInfo - every { kv.getCurrentSize() } returns size - every { - metadataManager.onPackageBackedUp( - pmPackageInfo, - BackupType.KV, - size, - metadataOutputStream, - ) - } just Runs - coEvery { kv.finishBackup() } returns TRANSPORT_OK - - // now check if we have opt-out apps that we need to back up APKs for - every { packageService.notBackedUpPackages } returns notAllowedPackages - // update notification - every { - notificationManager.onOptOutAppBackup( - notAllowedPackages[0].packageName, - 1, - notAllowedPackages.size - ) - } just Runs - // no backup needed - coEvery { - apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any()) - } returns null - // check old metadata for state changes, because we won't update it otherwise - every { - metadataManager.getPackageMetadata(notAllowedPackages[0].packageName) - } returns packageMetadata - every { packageMetadata.state } returns NOT_ALLOWED // no change - - // update notification for second package - every { - notificationManager.onOptOutAppBackup( - notAllowedPackages[1].packageName, - 2, - notAllowedPackages.size - ) - } just Runs - // was backed up, get new packageMetadata - coEvery { - apkBackup.backupApkIfNecessary(notAllowedPackages[1], WAS_STOPPED, any()) - } returns packageMetadata - every { settingsManager.getToken() } returns token - coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream - every { - metadataManager.onApkBackedUp( - notAllowedPackages[1], - packageMetadata, - metadataOutputStream - ) - } just Runs - every { metadataOutputStream.close() } just Runs - - assertEquals(TRANSPORT_OK, backup.finishBackup()) - - coVerify { - apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any()) - apkBackup.backupApkIfNecessary(notAllowedPackages[1], WAS_STOPPED, any()) - metadataOutputStream.close() - } - } - - @Test - fun `APK backup of not allowed apps updates state even without new APK`() = runBlocking { - val oldPackageMetadata: PackageMetadata = mockk() - - every { packageService.notBackedUpPackages } returns listOf(packageInfo) - every { - notificationManager.onOptOutAppBackup(packageInfo.packageName, 1, 1) - } just Runs - coEvery { apkBackup.backupApkIfNecessary(packageInfo, NOT_ALLOWED, any()) } returns null - every { - metadataManager.getPackageMetadata(packageInfo.packageName) - } returns oldPackageMetadata - // state differs now, was stopped before - every { oldPackageMetadata.state } returns WAS_STOPPED - every { settingsManager.getToken() } returns token - coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream - every { - metadataManager.onPackageBackupError( - packageInfo, - NOT_ALLOWED, - metadataOutputStream - ) - } just Runs - every { metadataOutputStream.close() } just Runs - - backup.backUpApksOfNotBackedUpPackages() - - verify { - metadataManager.onPackageBackupError(packageInfo, NOT_ALLOWED, metadataOutputStream) - metadataOutputStream.close() - } - } - - @Test - fun `APK backup of not allowed apps updates state even without old state`() = runBlocking { - every { packageService.notBackedUpPackages } returns listOf(packageInfo) - every { - notificationManager.onOptOutAppBackup(packageInfo.packageName, 1, 1) - } just Runs - coEvery { apkBackup.backupApkIfNecessary(packageInfo, NOT_ALLOWED, any()) } returns null - every { - metadataManager.getPackageMetadata(packageInfo.packageName) - } returns null - every { settingsManager.getToken() } returns token - coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream - every { - metadataManager.onPackageBackupError( - packageInfo, - NOT_ALLOWED, - metadataOutputStream - ) - } just Runs - every { metadataOutputStream.close() } just Runs - - backup.backUpApksOfNotBackedUpPackages() - - verify { - metadataManager.onPackageBackupError(packageInfo, NOT_ALLOWED, metadataOutputStream) - metadataOutputStream.close() - } } private fun expectApkBackupAndMetadataWrite() { - coEvery { - apkBackup.backupApkIfNecessary( - any(), - UNKNOWN_ERROR, - any() - ) - } returns packageMetadata + coEvery { apkBackup.backupApkIfNecessary(any(), any()) } returns packageMetadata every { settingsManager.getToken() } returns token coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream - every { - metadataManager.onApkBackedUp( - any(), - packageMetadata, - metadataOutputStream - ) - } just Runs + every { metadataManager.onApkBackedUp(any(), packageMetadata) } just Runs } } diff --git a/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt new file mode 100644 index 000000000..8f863d3d2 --- /dev/null +++ b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt @@ -0,0 +1,204 @@ +package com.stevesoltys.seedvault.worker + +import android.content.pm.ApplicationInfo +import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP +import android.content.pm.ApplicationInfo.FLAG_INSTALLED +import android.content.pm.ApplicationInfo.FLAG_STOPPED +import android.content.pm.PackageInfo +import com.stevesoltys.seedvault.metadata.PackageMetadata +import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED +import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR +import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED +import com.stevesoltys.seedvault.plugins.StoragePlugin +import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA +import com.stevesoltys.seedvault.transport.TransportTest +import com.stevesoltys.seedvault.transport.backup.PackageService +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify +import io.mockk.verifyAll +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import java.io.OutputStream + +internal class ApkBackupManagerTest : TransportTest() { + + private val packageService: PackageService = mockk() + private val apkBackup: ApkBackup = mockk() + private val plugin: StoragePlugin = mockk() + private val nm: BackupNotificationManager = mockk() + + private val apkBackupManager = ApkBackupManager( + context = context, + settingsManager = settingsManager, + metadataManager = metadataManager, + packageService = packageService, + apkBackup = apkBackup, + plugin = plugin, + nm = nm, + ) + + private val metadataOutputStream = mockk() + private val packageMetadata: PackageMetadata = mockk() + + @Test + fun `Package state of app that is not stopped gets recorded as not-allowed`() = runBlocking { + every { nm.onAppsNotBackedUp() } just Runs + every { packageService.notBackedUpPackages } returns listOf(packageInfo) + + every { + metadataManager.getPackageMetadata(packageInfo.packageName) + } returns packageMetadata + every { packageMetadata.state } returns UNKNOWN_ERROR + every { metadataManager.onPackageDoesNotGetBackedUp(packageInfo, NOT_ALLOWED) } just Runs + + every { settingsManager.backupApks() } returns false + expectFinalUpload() + every { nm.onApkBackupDone() } just Runs + + apkBackupManager.backup() + + verify { + metadataManager.onPackageDoesNotGetBackedUp(packageInfo, NOT_ALLOWED) + metadataOutputStream.close() + } + } + + @Test + fun `Package state of app gets recorded even if no previous state`() = runBlocking { + every { nm.onAppsNotBackedUp() } just Runs + every { packageService.notBackedUpPackages } returns listOf(packageInfo) + + every { + metadataManager.getPackageMetadata(packageInfo.packageName) + } returns null + every { metadataManager.onPackageDoesNotGetBackedUp(packageInfo, NOT_ALLOWED) } just Runs + + every { settingsManager.backupApks() } returns false + expectFinalUpload() + every { nm.onApkBackupDone() } just Runs + + apkBackupManager.backup() + + verify { + metadataManager.onPackageDoesNotGetBackedUp(packageInfo, NOT_ALLOWED) + metadataOutputStream.close() + } + } + + @Test + fun `Package state of app that is stopped gets recorded`() = runBlocking { + val packageInfo = PackageInfo().apply { + packageName = "org.example" + applicationInfo = mockk { + flags = FLAG_ALLOW_BACKUP or FLAG_INSTALLED or FLAG_STOPPED + } + } + + every { nm.onAppsNotBackedUp() } just Runs + every { packageService.notBackedUpPackages } returns listOf(packageInfo) + + every { + metadataManager.getPackageMetadata(packageInfo.packageName) + } returns packageMetadata + every { packageMetadata.state } returns UNKNOWN_ERROR + every { metadataManager.onPackageDoesNotGetBackedUp(packageInfo, WAS_STOPPED) } just Runs + + every { settingsManager.backupApks() } returns false + expectFinalUpload() + every { nm.onApkBackupDone() } just Runs + + apkBackupManager.backup() + + verify { + metadataManager.onPackageDoesNotGetBackedUp(packageInfo, WAS_STOPPED) + metadataOutputStream.close() + } + } + + @Test + fun `Package state only updated when changed`() = runBlocking { + every { nm.onAppsNotBackedUp() } just Runs + every { packageService.notBackedUpPackages } returns listOf(packageInfo) + + every { + metadataManager.getPackageMetadata(packageInfo.packageName) + } returns packageMetadata + every { packageMetadata.state } returns NOT_ALLOWED + + every { settingsManager.backupApks() } returns false + expectFinalUpload() + every { nm.onApkBackupDone() } just Runs + + apkBackupManager.backup() + + verifyAll(inverse = true) { + metadataManager.onPackageDoesNotGetBackedUp(packageInfo, NOT_ALLOWED) + } + } + + @Test + fun `two packages get backed up, one their APK uploaded`() = runBlocking { + val notAllowedPackages = listOf( + PackageInfo().apply { packageName = "org.example.1" }, + PackageInfo().apply { + packageName = "org.example.2" + // the second package does not get backed up, because it is stopped + applicationInfo = mockk { + flags = FLAG_STOPPED + } + } + ) + + expectAllAppsWillGetBackedUp() + every { settingsManager.backupApks() } returns true + + every { packageService.allUserPackages } returns notAllowedPackages + // update notification + every { + nm.onApkBackup(notAllowedPackages[0].packageName, any(), 0, notAllowedPackages.size) + } just Runs + // no backup needed + coEvery { + apkBackup.backupApkIfNecessary(notAllowedPackages[0], any()) + } returns null + // update notification for second package + every { + nm.onApkBackup(notAllowedPackages[1].packageName, any(), 1, notAllowedPackages.size) + } just Runs + // was backed up, get new packageMetadata + coEvery { + apkBackup.backupApkIfNecessary(notAllowedPackages[1], any()) + } returns packageMetadata + every { metadataManager.onApkBackedUp(notAllowedPackages[1], packageMetadata) } just Runs + + expectFinalUpload() + every { nm.onApkBackupDone() } just Runs + + apkBackupManager.backup() + + coVerify { + apkBackup.backupApkIfNecessary(notAllowedPackages[0], any()) + apkBackup.backupApkIfNecessary(notAllowedPackages[1], any()) + metadataOutputStream.close() + } + } + + private fun expectAllAppsWillGetBackedUp() { + every { nm.onAppsNotBackedUp() } just Runs + every { packageService.notBackedUpPackages } returns emptyList() + } + + private fun expectFinalUpload() { + every { settingsManager.getToken() } returns token + coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream + every { metadataManager.uploadMetadata(metadataOutputStream) } just Runs + every { metadataOutputStream.close() } just Runs + } + +} diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupTest.kt similarity index 90% rename from app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupTest.kt rename to app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupTest.kt index 0cbbf9e00..c56fcd247 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupTest.kt @@ -1,4 +1,9 @@ -package com.stevesoltys.seedvault.transport.backup +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.worker import android.content.pm.ApplicationInfo.FLAG_SYSTEM import android.content.pm.ApplicationInfo.FLAG_TEST_ONLY @@ -13,6 +18,7 @@ import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.metadata.ApkSplit import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR +import com.stevesoltys.seedvault.transport.backup.BackupTest import io.mockk.coEvery import io.mockk.every import io.mockk.mockk @@ -56,7 +62,7 @@ internal class ApkBackupTest : BackupTest() { @Test fun `does not back up @pm@`() = runBlocking { val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER } - assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) } @Test @@ -64,7 +70,7 @@ internal class ApkBackupTest : BackupTest() { every { settingsManager.backupApks() } returns false every { settingsManager.isBackupEnabled(any()) } returns true - assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) } @Test @@ -72,7 +78,7 @@ internal class ApkBackupTest : BackupTest() { every { settingsManager.backupApks() } returns true every { settingsManager.isBackupEnabled(any()) } returns false - assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) } @Test @@ -81,7 +87,7 @@ internal class ApkBackupTest : BackupTest() { every { settingsManager.isBackupEnabled(any()) } returns true every { settingsManager.backupApks() } returns true - assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) } @Test @@ -90,7 +96,7 @@ internal class ApkBackupTest : BackupTest() { every { settingsManager.isBackupEnabled(any()) } returns true every { settingsManager.backupApks() } returns true - assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) } @Test @@ -102,7 +108,7 @@ internal class ApkBackupTest : BackupTest() { expectChecks(packageMetadata) - assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) } @Test @@ -113,7 +119,7 @@ internal class ApkBackupTest : BackupTest() { assertThrows(IOException::class.java) { runBlocking { - assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) } } } @@ -128,7 +134,7 @@ internal class ApkBackupTest : BackupTest() { every { sigInfo.hasMultipleSigners() } returns false every { sigInfo.signingCertificateHistory } returns emptyArray() - assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) } @Test @@ -141,7 +147,7 @@ internal class ApkBackupTest : BackupTest() { }.absolutePath val apkOutputStream = ByteArrayOutputStream() val updatedMetadata = PackageMetadata( - time = 0L, + time = packageMetadata.time, state = UNKNOWN_ERROR, version = packageInfo.longVersionCode, installer = getRandomString(), @@ -159,7 +165,7 @@ internal class ApkBackupTest : BackupTest() { assertEquals( updatedMetadata, - apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter) + apkBackup.backupApkIfNecessary(packageInfo, streamGetter) ) assertArrayEquals(apkBytes, apkOutputStream.toByteArray()) } @@ -198,7 +204,7 @@ internal class ApkBackupTest : BackupTest() { val split2OutputStream = ByteArrayOutputStream() // expected new metadata for package val updatedMetadata = PackageMetadata( - time = 0L, + time = packageMetadata.time, state = UNKNOWN_ERROR, version = packageInfo.longVersionCode, installer = getRandomString(), @@ -231,7 +237,7 @@ internal class ApkBackupTest : BackupTest() { assertEquals( updatedMetadata, - apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter) + apkBackup.backupApkIfNecessary(packageInfo, streamGetter) ) assertArrayEquals(apkBytes, apkOutputStream.toByteArray()) assertArrayEquals(split1Bytes, split1OutputStream.toByteArray()) From 49066be31b05bd84d580bf0a009eed085909ef00 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 20 Feb 2024 15:45:58 -0300 Subject: [PATCH 03/76] Improve backup notification --- .../notification/BackupNotificationManager.kt | 12 ++++------- .../NotificationBackupObserver.kt | 20 ++++++++++++++----- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt index 9a97b4881..0a8bc09da 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt @@ -133,7 +133,7 @@ internal class BackupNotificationManager(private val context: Context) { */ fun onBackupStarted(expectedPackages: Int) { updateBackupNotification( - infoText = "", // This passes quickly, no need to show something here + appName = "", // This passes quickly, no need to show something here transferred = 0, expected = expectedPackages ) @@ -146,25 +146,21 @@ internal class BackupNotificationManager(private val context: Context) { */ fun onBackupUpdate(app: CharSequence, transferred: Int, total: Int) { updateBackupNotification( - infoText = app, + appName = app, transferred = min(transferred, total), expected = total ) } private fun updateBackupNotification( - infoText: CharSequence, + appName: CharSequence, transferred: Int, expected: Int, ) { - @Suppress("MagicNumber") - val percentage = (transferred.toFloat() / expected) * 100 - val percentageStr = "%.0f%%".format(percentage) - Log.i(TAG, "$transferred/$expected - $percentageStr - $infoText") val notification = Builder(context, CHANNEL_ID_OBSERVER).apply { setSmallIcon(R.drawable.ic_cloud_upload) setContentTitle(context.getString(R.string.notification_title)) - setContentText(percentageStr) + setContentText(appName) setOngoing(true) setShowWhen(false) setWhen(System.currentTimeMillis()) diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt index d253970a0..ff2982075 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt @@ -26,6 +26,7 @@ internal class NotificationBackupObserver( private val metadataManager: MetadataManager by inject() private var currentPackage: String? = null private var numPackages: Int = 0 + private var pmCounted: Boolean = false init { // Inform the notification manager that a backup has started @@ -93,13 +94,22 @@ internal class NotificationBackupObserver( ) currentPackage = packageName val appName = getAppName(packageName) - val app = if (appName != packageName) { - "${getAppName(packageName)} ($packageName)" + val name = if (appName != packageName) { + appName } else { - packageName + context.getString(R.string.backup_section_system) } - numPackages += 1 - nm.onBackupUpdate(app, numPackages, requestedPackages) + // prevent double counting of @pm@ which gets backed up with each requested chunk + if (packageName == MAGIC_PACKAGE_MANAGER) { + if (!pmCounted) { + numPackages += 1 + pmCounted = true + } + } else { + numPackages += 1 + } + Log.i(TAG, "$numPackages/$requestedPackages - $appName ($packageName)") + nm.onBackupUpdate(name, numPackages, requestedPackages) } private fun getAppName(packageId: String): CharSequence = getAppName(context, packageId) From 8da73ad8d1ceb7df44a0b00b20b7005768e73bef Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 20 Feb 2024 11:52:49 -0300 Subject: [PATCH 04/76] Make 'Backup now' action use AppBackupWorker --- .../seedvault/KoinInstrumentationTestApp.kt | 2 +- app/src/main/AndroidManifest.xml | 5 + .../java/com/stevesoltys/seedvault/App.kt | 4 +- .../com/stevesoltys/seedvault/BackupWorker.kt | 57 --------- .../seedvault/UsbIntentReceiver.kt | 6 +- .../seedvault/restore/RestoreViewModel.kt | 2 +- .../seedvault/settings/SettingsViewModel.kt | 9 +- .../stevesoltys/seedvault/storage/Services.kt | 4 +- .../ConfigurableBackupTransportService.kt | 25 ---- .../notification/BackupNotificationManager.kt | 87 ++++--------- .../NotificationBackupObserver.kt | 2 +- .../ui/storage/BackupStorageViewModel.kt | 6 +- .../seedvault/worker/AppBackupWorker.kt | 118 ++++++++++++++++++ .../backup => worker}/BackupRequester.kt | 5 +- .../seedvault/worker/WorkerModule.kt | 7 ++ app/src/main/res/values/strings.xml | 1 - 16 files changed, 172 insertions(+), 168 deletions(-) delete mode 100644 app/src/main/java/com/stevesoltys/seedvault/BackupWorker.kt create mode 100644 app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt rename app/src/main/java/com/stevesoltys/seedvault/{transport/backup => worker}/BackupRequester.kt (95%) diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt index c00438f20..6a2e56013 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt @@ -47,7 +47,7 @@ class KoinInstrumentationTestApp : App() { viewModel { currentBackupStorageViewModel = - spyk(BackupStorageViewModel(context, get(), get(), get(), get())) + spyk(BackupStorageViewModel(context, get(), get(), get())) currentBackupStorageViewModel!! } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bc9213ab..c3bd194cc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -156,6 +156,11 @@ + + ( - repeatInterval = 24, - repeatIntervalTimeUnit = TimeUnit.HOURS, - flexTimeInterval = 2, - flexTimeIntervalUnit = TimeUnit.HOURS, - ).setConstraints(backupConstraints) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.HOURS) - .build() - val workManager = WorkManager.getInstance(appContext) - workManager.enqueueUniquePeriodicWork(UNIQUE_WORK_NAME, UPDATE, backupWorkRequest) - } - - fun unschedule(appContext: Context) { - val workManager = WorkManager.getInstance(appContext) - workManager.cancelUniqueWork(UNIQUE_WORK_NAME) - } - } - - override fun doWork(): Result { - // TODO once we make this the default, we should do storage backup here as well - // or have two workers and ensure they never run at the same time - return if (requestBackup(applicationContext)) Result.success() - else Result.retry() - } -} diff --git a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt index 4800fcefb..ff5208b03 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt @@ -20,8 +20,8 @@ import com.stevesoltys.seedvault.settings.FlashDrive import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.storage.StorageBackupService import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP -import com.stevesoltys.seedvault.transport.requestBackup import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE +import com.stevesoltys.seedvault.worker.AppBackupWorker import org.koin.core.context.GlobalContext.get import java.util.concurrent.TimeUnit.HOURS @@ -63,9 +63,7 @@ class UsbIntentReceiver : UsbMonitor() { i.putExtra(EXTRA_START_APP_BACKUP, true) startForegroundService(context, i) } else { - Thread { - requestBackup(context) - }.start() + AppBackupWorker.scheduleNow(context) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt index 87d4b0194..c7eaee3a9 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt @@ -39,7 +39,7 @@ import com.stevesoltys.seedvault.restore.install.isInstalled import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.storage.StorageRestoreService import com.stevesoltys.seedvault.transport.TRANSPORT_ID -import com.stevesoltys.seedvault.transport.backup.NUM_PACKAGES_PER_TRANSACTION +import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator import com.stevesoltys.seedvault.ui.AppBackupState import com.stevesoltys.seedvault.ui.AppBackupState.FAILED diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index f56faa2ff..6508aa7c5 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -25,7 +25,6 @@ import androidx.lifecycle.liveData import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope import androidx.recyclerview.widget.DiffUtil.calculateDiff -import com.stevesoltys.seedvault.BackupWorker import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.metadata.MetadataManager @@ -33,9 +32,9 @@ import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.storage.StorageBackupJobService import com.stevesoltys.seedvault.storage.StorageBackupService import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP -import com.stevesoltys.seedvault.transport.requestBackup import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import com.stevesoltys.seedvault.worker.AppBackupWorker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -175,7 +174,7 @@ internal class SettingsViewModel( i.putExtra(EXTRA_START_APP_BACKUP, true) startForegroundService(app, i) } else { - requestBackup(app) + AppBackupWorker.scheduleNow(app) } } } @@ -267,9 +266,9 @@ internal class SettingsViewModel( fun onD2dChanged(enabled: Boolean) { backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), !enabled) if (enabled) { - BackupWorker.schedule(app) + AppBackupWorker.schedule(app) } else { - BackupWorker.unschedule(app) + AppBackupWorker.unschedule(app) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt b/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt index 1c54beb27..e42da2d3f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt @@ -1,7 +1,7 @@ package com.stevesoltys.seedvault.storage import android.content.Intent -import com.stevesoltys.seedvault.transport.requestBackup +import com.stevesoltys.seedvault.worker.AppBackupWorker import org.calyxos.backup.storage.api.BackupObserver import org.calyxos.backup.storage.api.RestoreObserver import org.calyxos.backup.storage.api.StorageBackup @@ -40,7 +40,7 @@ internal class StorageBackupService : BackupService() { override fun onBackupFinished(intent: Intent, success: Boolean) { if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) { - requestBackup(applicationContext) + AppBackupWorker.scheduleNow(applicationContext) } } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt index 1b9fe3b6c..9d81d3e5e 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt @@ -2,18 +2,13 @@ package com.stevesoltys.seedvault.transport import android.app.Service import android.app.backup.IBackupManager -import android.content.Context import android.content.Intent import android.os.IBinder import android.util.Log -import androidx.annotation.WorkerThread import com.stevesoltys.seedvault.crypto.KeyManager -import com.stevesoltys.seedvault.transport.backup.BackupRequester -import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import org.koin.core.context.GlobalContext.get private val TAG = ConfigurableBackupTransportService::class.java.simpleName @@ -56,23 +51,3 @@ class ConfigurableBackupTransportService : Service(), KoinComponent { } } - -/** - * Requests the system to initiate a backup. - * - * @return true iff backups was requested successfully (backup itself can still fail). - */ -@WorkerThread -fun requestBackup(context: Context): Boolean { - val backupManager: IBackupManager = get().get() - return if (backupManager.isBackupEnabled) { - val packageService: PackageService = get().get() - - Log.d(TAG, "Backup is enabled, request backup...") - val backupRequester = BackupRequester(context, backupManager, packageService) - return backupRequester.requestBackup() - } else { - Log.i(TAG, "Backup is not enabled") - true // this counts as success - } -} diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt index 0a8bc09da..bc3fb55d7 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt @@ -30,17 +30,15 @@ import com.stevesoltys.seedvault.settings.SettingsActivity import kotlin.math.min private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver" -private const val CHANNEL_ID_APK = "NotificationApkBackup" private const val CHANNEL_ID_SUCCESS = "NotificationBackupSuccess" private const val CHANNEL_ID_ERROR = "NotificationError" private const val CHANNEL_ID_RESTORE_ERROR = "NotificationRestoreError" -private const val NOTIFICATION_ID_OBSERVER = 1 -internal const val NOTIFICATION_ID_APK = 2 -private const val NOTIFICATION_ID_SUCCESS = 3 -private const val NOTIFICATION_ID_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_OBSERVER = 1 +private const val NOTIFICATION_ID_SUCCESS = 2 +private const val NOTIFICATION_ID_ERROR = 3 +private const val NOTIFICATION_ID_RESTORE_ERROR = 4 +private const val NOTIFICATION_ID_BACKGROUND = 5 +private const val NOTIFICATION_ID_NO_MAIN_KEY_ERROR = 6 private val TAG = BackupNotificationManager::class.java.simpleName @@ -48,7 +46,6 @@ internal class BackupNotificationManager(private val context: Context) { private val nm = context.getSystemService(NotificationManager::class.java)!!.apply { createNotificationChannel(getObserverChannel()) - createNotificationChannel(getApkChannel()) createNotificationChannel(getSuccessChannel()) createNotificationChannel(getErrorChannel()) createNotificationChannel(getRestoreErrorChannel()) @@ -61,13 +58,6 @@ internal class BackupNotificationManager(private val context: Context) { } } - private fun getApkChannel(): NotificationChannel { - val title = context.getString(R.string.notification_apk_channel_title) - return NotificationChannel(CHANNEL_ID_APK, title, IMPORTANCE_LOW).apply { - enableVibration(false) - } - } - private fun getSuccessChannel(): NotificationChannel { val title = context.getString(R.string.notification_success_channel_title) return NotificationChannel(CHANNEL_ID_SUCCESS, title, IMPORTANCE_LOW).apply { @@ -91,8 +81,7 @@ internal class BackupNotificationManager(private val context: Context) { fun onApkBackup(packageName: String, name: CharSequence, transferred: Int, expected: Int) { Log.i(TAG, "$transferred/$expected - $name ($packageName)") val text = context.getString(R.string.notification_apk_text, name) - val notification = getApkBackupNotification(text, transferred, expected) - nm.notify(NOTIFICATION_ID_APK, notification) + updateBackupNotification(text, transferred, expected) } /** @@ -100,32 +89,15 @@ internal class BackupNotificationManager(private val context: Context) { */ fun onAppsNotBackedUp() { Log.i(TAG, "onAppsNotBackedUp") - val notification = - getApkBackupNotification(context.getString(R.string.notification_apk_not_backed_up)) - nm.notify(NOTIFICATION_ID_APK, notification) + val text = context.getString(R.string.notification_apk_not_backed_up) + updateBackupNotification(text) } - fun getApkBackupNotification( - text: String?, - expected: Int = 0, - transferred: Int = 0, - ): Notification = Builder(context, CHANNEL_ID_APK).apply { - setSmallIcon(R.drawable.ic_cloud_upload) - setContentTitle(context.getString(R.string.notification_title)) - setContentText(text) - setOngoing(true) - setShowWhen(false) - setWhen(System.currentTimeMillis()) - setProgress(expected, transferred, false) - priority = PRIORITY_DEFAULT - foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE - }.build() - /** * Call after [onApkBackup] or [onAppsNotBackedUp] were called. */ fun onApkBackupDone() { - nm.cancel(NOTIFICATION_ID_APK) + nm.cancel(NOTIFICATION_ID_OBSERVER) } /** @@ -133,7 +105,7 @@ internal class BackupNotificationManager(private val context: Context) { */ fun onBackupStarted(expectedPackages: Int) { updateBackupNotification( - appName = "", // This passes quickly, no need to show something here + text = "", // This passes quickly, no need to show something here transferred = 0, expected = expectedPackages ) @@ -145,44 +117,29 @@ internal class BackupNotificationManager(private val context: Context) { * this type is is expected to get called after [onApkBackup]. */ fun onBackupUpdate(app: CharSequence, transferred: Int, total: Int) { - updateBackupNotification( - appName = app, - transferred = min(transferred, total), - expected = total - ) + updateBackupNotification(app, min(transferred, total), total) } private fun updateBackupNotification( - appName: CharSequence, - transferred: Int, - expected: Int, + text: CharSequence, + transferred: Int = 0, + expected: Int = 0, ) { - val notification = Builder(context, CHANNEL_ID_OBSERVER).apply { - setSmallIcon(R.drawable.ic_cloud_upload) - setContentTitle(context.getString(R.string.notification_title)) - setContentText(appName) - setOngoing(true) - setShowWhen(false) - setWhen(System.currentTimeMillis()) - setProgress(expected, transferred, false) - priority = PRIORITY_DEFAULT - foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE - }.build() + val notification = getBackupNotification(text, transferred, expected) nm.notify(NOTIFICATION_ID_OBSERVER, notification) } - private fun updateBackgroundBackupNotification(infoText: CharSequence) { - Log.i(TAG, "$infoText") - val notification = Builder(context, CHANNEL_ID_OBSERVER).apply { + fun getBackupNotification(text: CharSequence, progress: Int = 0, total: Int = 0): Notification { + return Builder(context, CHANNEL_ID_OBSERVER).apply { setSmallIcon(R.drawable.ic_cloud_upload) setContentTitle(context.getString(R.string.notification_title)) + setContentText(text) setOngoing(true) setShowWhen(false) - setWhen(System.currentTimeMillis()) - setProgress(0, 0, true) - priority = PRIORITY_LOW + setProgress(total, progress, progress == 0 && total == 0) + priority = PRIORITY_DEFAULT + foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE }.build() - nm.notify(NOTIFICATION_ID_BACKGROUND, notification) } fun onServiceDestroyed() { diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt index ff2982075..5f5eaea4f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt @@ -10,7 +10,7 @@ import android.util.Log.isLoggable import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.metadata.MetadataManager -import com.stevesoltys.seedvault.transport.backup.BackupRequester +import com.stevesoltys.seedvault.worker.BackupRequester import org.koin.core.component.KoinComponent import org.koin.core.component.inject diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt index 4595468b0..33fe51b80 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt @@ -12,8 +12,7 @@ import androidx.lifecycle.viewModelScope import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.TRANSPORT_ID -import com.stevesoltys.seedvault.transport.backup.BackupCoordinator -import com.stevesoltys.seedvault.transport.requestBackup +import com.stevesoltys.seedvault.worker.AppBackupWorker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.calyxos.backup.storage.api.StorageBackup @@ -24,7 +23,6 @@ private val TAG = BackupStorageViewModel::class.java.simpleName internal class BackupStorageViewModel( private val app: Application, private val backupManager: IBackupManager, - private val backupCoordinator: BackupCoordinator, private val storageBackup: StorageBackup, settingsManager: SettingsManager, ) : StorageViewModel(app, settingsManager) { @@ -73,7 +71,7 @@ internal class BackupStorageViewModel( // notify the UI that the location has been set mLocationChecked.postEvent(LocationResult()) if (requestBackup) { - requestBackup(app) + AppBackupWorker.scheduleNow(app) } } else { // notify the UI that the location was invalid diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt new file mode 100644 index 000000000..f6cc1b459 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.worker + +import android.content.Context +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC +import android.util.Log +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE +import androidx.work.ExistingPeriodicWorkPolicy.UPDATE +import androidx.work.ExistingWorkPolicy.REPLACE +import androidx.work.ForegroundInfo +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import com.stevesoltys.seedvault.ui.notification.NOTIFICATION_ID_OBSERVER +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.util.concurrent.TimeUnit + +class AppBackupWorker( + appContext: Context, + workerParams: WorkerParameters, +) : CoroutineWorker(appContext, workerParams), KoinComponent { + + companion object { + private val TAG = AppBackupWorker::class.simpleName + private const val UNIQUE_WORK_NAME = "com.stevesoltys.seedvault.APP_BACKUP" + private const val TAG_NOW = "com.stevesoltys.seedvault.TAG_NOW" + + fun schedule(context: Context, existingWorkPolicy: ExistingPeriodicWorkPolicy = UPDATE) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.UNMETERED) + .setRequiresCharging(true) + .build() + val workRequest = PeriodicWorkRequestBuilder( + repeatInterval = 24, + repeatIntervalTimeUnit = TimeUnit.HOURS, + flexTimeInterval = 2, + flexTimeIntervalUnit = TimeUnit.HOURS, + ).setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES) + .build() + val workManager = WorkManager.getInstance(context) + Log.i(TAG, "Scheduling app backup: $workRequest") + workManager.enqueueUniquePeriodicWork(UNIQUE_WORK_NAME, existingWorkPolicy, workRequest) + } + + fun scheduleNow(context: Context) { + val workRequest = OneTimeWorkRequestBuilder() + .setExpedited(RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .addTag(TAG_NOW) + .build() + val workManager = WorkManager.getInstance(context) + Log.i(TAG, "Asking to do app backup now...") + workManager.enqueueUniqueWork(UNIQUE_WORK_NAME, REPLACE, workRequest) + } + + fun unschedule(context: Context) { + Log.i(TAG, "Unscheduling app backup...") + val workManager = WorkManager.getInstance(context) + workManager.cancelUniqueWork(UNIQUE_WORK_NAME) + } + } + + private val backupRequester: BackupRequester by inject() + private val apkBackupManager: ApkBackupManager by inject() + private val nm: BackupNotificationManager by inject() + + override suspend fun doWork(): Result { + try { + setForeground(createForegroundInfo()) + } catch (e: Exception) { + Log.e(TAG, "Error while running setForeground: ", e) + } + var result: Result = Result.success() + try { + Log.i(TAG, "Starting APK backup...") + apkBackupManager.backup() + } catch (e: Exception) { + Log.e(TAG, "Error backing up APKs: ", e) + result = Result.retry() + } finally { + Log.i(TAG, "Requesting app data backup...") + val requestSuccess = try { + if (backupRequester.isBackupEnabled) { + Log.d(TAG, "Backup is enabled, request backup...") + backupRequester.requestBackup() + } else true + } finally { + // schedule next backup, because the old one gets lost + // when scheduling a OneTimeWorkRequest with the same unique name via scheduleNow() + if (tags.contains(TAG_NOW)) { + // needs to use CANCEL_AND_REENQUEUE otherwise it doesn't get scheduled + schedule(applicationContext, CANCEL_AND_REENQUEUE) + } + } + if (!requestSuccess) result = Result.retry() + } + return result + } + + private fun createForegroundInfo() = ForegroundInfo( + NOTIFICATION_ID_OBSERVER, + nm.getBackupNotification(""), + FOREGROUND_SERVICE_TYPE_DATA_SYNC, + ) +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupRequester.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/BackupRequester.kt similarity index 95% rename from app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupRequester.kt rename to app/src/main/java/com/stevesoltys/seedvault/worker/BackupRequester.kt index 209db7622..9eac74069 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupRequester.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/BackupRequester.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package com.stevesoltys.seedvault.transport.backup +package com.stevesoltys.seedvault.worker import android.app.backup.BackupManager import android.app.backup.IBackupManager @@ -12,6 +12,7 @@ import android.os.RemoteException import android.util.Log import androidx.annotation.WorkerThread import com.stevesoltys.seedvault.BackupMonitor +import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver import org.koin.core.component.KoinComponent @@ -34,6 +35,8 @@ internal class BackupRequester( val packageService: PackageService, ) : KoinComponent { + val isBackupEnabled: Boolean get() = backupManager.isBackupEnabled + private val packages = packageService.eligiblePackages private val observer = NotificationBackupObserver( context = context, diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt index ed18a6352..dce45be2d 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt @@ -9,6 +9,13 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val workerModule = module { + factory { + BackupRequester( + context = androidContext(), + backupManager = get(), + packageService = get(), + ) + } single { ApkBackup( pm = androidContext().packageManager, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e8283aafe..44097e712 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -119,7 +119,6 @@ Backup notification - APK backup notification Success notification Backup running Backing up APK of %s From 911a8dabf4e3625321671d19c585cdd64d0c760c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 21 Feb 2024 14:53:15 -0300 Subject: [PATCH 05/76] Expose time of next backup in UI to help debugging scheduling issues --- .../seedvault/settings/SettingsFragment.kt | 49 +++++++++++++++++-- .../seedvault/settings/SettingsViewModel.kt | 9 ++++ .../seedvault/worker/AppBackupWorker.kt | 2 +- app/src/main/res/values/strings.xml | 2 + 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt index 8115c5339..466107c03 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -140,7 +140,10 @@ class SettingsFragment : PreferenceFragmentCompat() { super.onViewCreated(view, savedInstanceState) viewModel.lastBackupTime.observe(viewLifecycleOwner) { time -> - setAppBackupStatusSummary(time) + setAppBackupStatusSummary(time, viewModel.nextScheduleTimeMillis.value) + } + viewModel.nextScheduleTimeMillis.observe(viewLifecycleOwner) { time -> + setAppBackupStatusSummary(viewModel.lastBackupTime.value, time) } val backupFiles: Preference = findPreference("backup_files")!! @@ -159,6 +162,10 @@ class SettingsFragment : PreferenceFragmentCompat() { setBackupEnabledState() setBackupLocationSummary() setAutoRestoreState() + setAppBackupStatusSummary( + lastBackupInMillis = viewModel.lastBackupTime.value, + nextScheduleTimeMillis = viewModel.nextScheduleTimeMillis.value, + ) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -244,10 +251,42 @@ class SettingsFragment : PreferenceFragmentCompat() { backupLocation.summary = storage?.name ?: getString(R.string.settings_backup_location_none) } - private fun setAppBackupStatusSummary(lastBackupInMillis: Long) { - // set time of last backup - val lastBackup = lastBackupInMillis.toRelativeTime(requireContext()) - backupStatus.summary = getString(R.string.settings_backup_status_summary, lastBackup) + private fun setAppBackupStatusSummary( + lastBackupInMillis: Long?, + nextScheduleTimeMillis: Long?, + ) { + val sb = StringBuilder() + if (lastBackupInMillis != null) { + // set time of last backup + val lastBackup = lastBackupInMillis.toRelativeTime(requireContext()) + sb.append(getString(R.string.settings_backup_status_summary, lastBackup)) + } + if (nextScheduleTimeMillis != null) { + // insert linebreak, if we have text before + if (sb.isNotEmpty()) sb.append("\n") + // set time of next backup + when (nextScheduleTimeMillis) { + -1L -> { + val text = getString(R.string.settings_backup_last_backup_never) + sb.append(getString(R.string.settings_backup_status_next_backup, text)) + } + + Long.MAX_VALUE -> { + val text = if (backupManager.isBackupEnabled) { + getString(R.string.notification_title) + } else { + getString(R.string.settings_backup_last_backup_never) + } + sb.append(getString(R.string.settings_backup_status_next_backup, text)) + } + + else -> { + val text = nextScheduleTimeMillis.toRelativeTime(requireContext()) + sb.append(getString(R.string.settings_backup_status_next_backup_estimate, text)) + } + } + } + backupStatus.summary = sb.toString() } private fun onEnablingStorageBackup() { diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index 6508aa7c5..d241e9190 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -22,9 +22,11 @@ import androidx.core.content.ContextCompat.startForegroundService import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.liveData +import androidx.lifecycle.map import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope import androidx.recyclerview.widget.DiffUtil.calculateDiff +import androidx.work.WorkManager import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.metadata.MetadataManager @@ -35,6 +37,7 @@ import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_ST import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.worker.AppBackupWorker +import com.stevesoltys.seedvault.worker.AppBackupWorker.Companion.UNIQUE_WORK_NAME import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -61,6 +64,7 @@ internal class SettingsViewModel( private val contentResolver = app.contentResolver private val connectivityManager: ConnectivityManager? = app.getSystemService(ConnectivityManager::class.java) + private val workManager = WorkManager.getInstance(app) override val isRestoreOperation = false @@ -68,6 +72,11 @@ internal class SettingsViewModel( val backupPossible: LiveData = mBackupPossible internal val lastBackupTime = metadataManager.lastBackupTime + val nextScheduleTimeMillis = + workManager.getWorkInfosForUniqueWorkLiveData(UNIQUE_WORK_NAME).map { + if (it.size > 0) it[0].nextScheduleTimeMillis + else -1L + } private val mAppStatusList = lastBackupTime.switchMap { // updates app list when lastBackupTime changes diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt index f6cc1b459..43f53d95b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt @@ -35,7 +35,7 @@ class AppBackupWorker( companion object { private val TAG = AppBackupWorker::class.simpleName - private const val UNIQUE_WORK_NAME = "com.stevesoltys.seedvault.APP_BACKUP" + internal const val UNIQUE_WORK_NAME = "com.stevesoltys.seedvault.APP_BACKUP" private const val TAG_NOW = "com.stevesoltys.seedvault.TAG_NOW" fun schedule(context: Context, existingWorkPolicy: ExistingPeriodicWorkPolicy = UPDATE) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 44097e712..13c094361 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,6 +30,8 @@ Disable app backup Backup status Last backup: %1$s + Next backup: %1$s + Next backup (estimate): %1$s Exclude apps Backup now Storage backup (beta) From 04fc90e9f70140ea7e3f314fb3f1070b525f9555 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 21 Feb 2024 17:08:35 -0300 Subject: [PATCH 06/76] Migrate to own backup scheduling --- .../java/com/stevesoltys/seedvault/App.kt | 25 +++++++ .../settings/ExpertSettingsFragment.kt | 3 +- .../seedvault/settings/SettingsFragment.kt | 14 +++- .../seedvault/settings/SettingsViewModel.kt | 68 ++++++++++--------- .../seedvault/worker/AppBackupWorker.kt | 30 ++++---- .../java/com/stevesoltys/seedvault/TestApp.kt | 2 + 6 files changed, 91 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index c65dc9f7e..af6ac99a7 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -9,7 +9,10 @@ import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Build import android.os.ServiceManager.getService import android.os.StrictMode +import android.os.UserHandle import android.os.UserManager +import android.provider.Settings +import androidx.work.WorkManager import com.stevesoltys.seedvault.crypto.cryptoModule import com.stevesoltys.seedvault.header.headerModule import com.stevesoltys.seedvault.metadata.MetadataManager @@ -28,6 +31,7 @@ import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel +import com.stevesoltys.seedvault.worker.AppBackupWorker import com.stevesoltys.seedvault.worker.workerModule import org.koin.android.ext.android.inject import org.koin.android.ext.koin.androidContext @@ -43,6 +47,8 @@ import org.koin.dsl.module */ open class App : Application() { + open val isTest: Boolean = false + private val appModule = module { single { SettingsManager(this@App) } single { BackupNotificationManager(this@App) } @@ -79,6 +85,7 @@ open class App : Application() { permitDiskReads { migrateTokenFromMetadataToSettingsManager() } + if (!isTest) migrateToOwnScheduling() } protected open fun startKoin() = startKoin { @@ -102,6 +109,7 @@ open class App : Application() { private val settingsManager: SettingsManager by inject() private val metadataManager: MetadataManager by inject() + private val backupManager: IBackupManager by inject() /** * The responsibility for the current token was moved to the [SettingsManager] @@ -117,6 +125,23 @@ open class App : Application() { } } + /** + * Disables the framework scheduling in favor of our own. + * Introduced in the first half of 2024 and can be removed after a suitable migration period. + */ + protected open fun migrateToOwnScheduling() { + if (!isFrameworkSchedulingEnabled()) return // already on own scheduling + + backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false) + if (backupManager.isBackupEnabled) AppBackupWorker.schedule(applicationContext) + // cancel old D2D worker + WorkManager.getInstance(this).cancelUniqueWork("APP_BACKUP") + } + + 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 = PACKAGE_MANAGER_SENTINEL diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt index b1281325e..c7e7d3785 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt @@ -44,8 +44,7 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() { val d2dPreference = findPreference(PREF_KEY_D2D_BACKUPS) d2dPreference?.setOnPreferenceChangeListener { _, newValue -> - viewModel.onD2dChanged(newValue as Boolean) - d2dPreference.isChecked = newValue + d2dPreference.isChecked = newValue as Boolean // automatically enable unlimited quota when enabling D2D backups if (d2dPreference.isChecked) { diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt index 466107c03..08648a8e2 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -23,6 +23,7 @@ import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.restore.RestoreActivity import com.stevesoltys.seedvault.ui.toRelativeTime +import com.stevesoltys.seedvault.worker.AppBackupWorker import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.sharedViewModel @@ -125,8 +126,9 @@ class SettingsFragment : PreferenceFragmentCompat() { backupStorage = findPreference("backup_storage")!! backupStorage.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> val disable = !(newValue as Boolean) + // TODO this should really get moved out off the UI layer if (disable) { - viewModel.disableStorageBackup() + viewModel.cancelBackupWorkers() return@OnPreferenceChangeListener true } onEnablingStorageBackup() @@ -208,10 +210,16 @@ class SettingsFragment : PreferenceFragmentCompat() { else -> super.onOptionsItemSelected(item) } + // TODO this should really get moved out off the UI layer private fun trySetBackupEnabled(enabled: Boolean): Boolean { return try { backupManager.isBackupEnabled = enabled - if (enabled) viewModel.enableCallLogBackup() + if (enabled) { + AppBackupWorker.schedule(requireContext()) + viewModel.enableCallLogBackup() + } else { + AppBackupWorker.unschedule(requireContext()) + } backup.isChecked = enabled true } catch (e: RemoteException) { @@ -307,7 +315,7 @@ class SettingsFragment : PreferenceFragmentCompat() { LENGTH_LONG ).show() } - viewModel.enableStorageBackup() + viewModel.scheduleBackupWorkers() backupStorage.isChecked = true dialog.dismiss() } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index d241e9190..eda7c4f71 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -12,7 +12,6 @@ import android.net.NetworkCapabilities import android.net.NetworkRequest import android.net.Uri import android.os.Process.myUid -import android.os.UserHandle import android.provider.Settings import android.util.Log import android.widget.Toast @@ -92,19 +91,19 @@ internal class SettingsViewModel( private val storageObserver = object : ContentObserver(null) { override fun onChange(selfChange: Boolean, uris: MutableCollection, flags: Int) { - onStorageLocationChanged() + onStoragePropertiesChanged() } } private inner class NetworkObserver : ConnectivityManager.NetworkCallback() { var registered = false override fun onAvailable(network: Network) { - onStorageLocationChanged() + onStoragePropertiesChanged() } override fun onLost(network: Network) { super.onLost(network) - onStorageLocationChanged() + onStoragePropertiesChanged() } } @@ -119,13 +118,29 @@ internal class SettingsViewModel( // ensures the lastBackupTime LiveData gets set metadataManager.getLastBackupTime() } - onStorageLocationChanged() + onStoragePropertiesChanged() loadFilesSummary() } override fun onStorageLocationChanged() { val storage = settingsManager.getStorage() ?: return + Log.i(TAG, "onStorageLocationChanged") + if (storage.isUsb) { + // disable storage backup if new storage is on USB + cancelBackupWorkers() + } else { + // enable it, just in case the previous storage was on USB, + // also to update the network requirement of the new storage + scheduleBackupWorkers() + } + onStoragePropertiesChanged() + } + + private fun onStoragePropertiesChanged() { + val storage = settingsManager.getStorage() ?: return + + Log.d(TAG, "onStoragePropertiesChanged") // register storage observer try { contentResolver.unregisterContentObserver(storageObserver) @@ -148,14 +163,6 @@ internal class SettingsViewModel( networkCallback.registered = true } - if (settingsManager.isStorageBackupEnabled()) { - // disable storage backup if new storage is on USB - if (storage.isUsb) disableStorageBackup() - // enable it, just in case the previous storage was on USB, - // also to update the network requirement of the new storage - else enableStorageBackup() - } - viewModelScope.launch(Dispatchers.IO) { val canDo = settingsManager.canDoBackupNow() mBackupPossible.postValue(canDo) @@ -231,20 +238,24 @@ internal class SettingsViewModel( return keyManager.hasMainKey() } - fun enableStorageBackup() { + fun scheduleBackupWorkers() { val storage = settingsManager.getStorage() ?: error("no storage available") - if (!storage.isUsb) BackupJobService.scheduleJob( - context = app, - jobServiceClass = StorageBackupJobService::class.java, - periodMillis = HOURS.toMillis(24), - networkType = if (storage.requiresNetwork) NETWORK_TYPE_UNMETERED - else NETWORK_TYPE_NONE, - deviceIdle = false, - charging = true - ) + if (!storage.isUsb) { + if (backupManager.isBackupEnabled) AppBackupWorker.schedule(app) + if (settingsManager.isStorageBackupEnabled()) BackupJobService.scheduleJob( + context = app, + jobServiceClass = StorageBackupJobService::class.java, + periodMillis = HOURS.toMillis(24), + networkType = if (storage.requiresNetwork) NETWORK_TYPE_UNMETERED + else NETWORK_TYPE_NONE, + deviceIdle = false, + charging = true + ) + } } - fun disableStorageBackup() { + fun cancelBackupWorkers() { + AppBackupWorker.unschedule(app) BackupJobService.cancelJob(app) } @@ -272,13 +283,4 @@ internal class SettingsViewModel( Toast.makeText(app, str, LENGTH_LONG).show() } - fun onD2dChanged(enabled: Boolean) { - backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), !enabled) - if (enabled) { - AppBackupWorker.schedule(app) - } else { - AppBackupWorker.unschedule(app) - } - } - } diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt index 43f53d95b..96ac383fe 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt @@ -83,6 +83,19 @@ class AppBackupWorker( } catch (e: Exception) { Log.e(TAG, "Error while running setForeground: ", e) } + return try { + doBackup() + } finally { + // schedule next backup, because the old one gets lost + // when scheduling a OneTimeWorkRequest with the same unique name via scheduleNow() + if (tags.contains(TAG_NOW) && backupRequester.isBackupEnabled) { + // needs to use CANCEL_AND_REENQUEUE otherwise it doesn't get scheduled + schedule(applicationContext, CANCEL_AND_REENQUEUE) + } + } + } + + private suspend fun doBackup(): Result { var result: Result = Result.success() try { Log.i(TAG, "Starting APK backup...") @@ -92,19 +105,10 @@ class AppBackupWorker( result = Result.retry() } finally { Log.i(TAG, "Requesting app data backup...") - val requestSuccess = try { - if (backupRequester.isBackupEnabled) { - Log.d(TAG, "Backup is enabled, request backup...") - backupRequester.requestBackup() - } else true - } finally { - // schedule next backup, because the old one gets lost - // when scheduling a OneTimeWorkRequest with the same unique name via scheduleNow() - if (tags.contains(TAG_NOW)) { - // needs to use CANCEL_AND_REENQUEUE otherwise it doesn't get scheduled - schedule(applicationContext, CANCEL_AND_REENQUEUE) - } - } + val requestSuccess = if (backupRequester.isBackupEnabled) { + Log.d(TAG, "Backup is enabled, request backup...") + backupRequester.requestBackup() + } else true if (!requestSuccess) result = Result.retry() } return result diff --git a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt index cdf03aeae..41d6e53b0 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt @@ -19,6 +19,8 @@ import org.koin.dsl.module class TestApp : App() { + override val isTest: Boolean = true + private val testCryptoModule = module { factory { CipherFactoryImpl(get()) } single { KeyManagerTestImpl() } From 0d7156789e9abe6aa9e675e07a40a92d34e4049d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 21 Feb 2024 17:25:13 -0300 Subject: [PATCH 07/76] Guard against BadParcelableException when getting app list hopefully something rare, but it just happened to me while testing. It seems it happens when there are many apps installed (>500) and the app list is open while a backup happens. Then, we keep reloading the list and hammer the package manager hard which it seems can't handle it. It does recover on its own though. --- .../stevesoltys/seedvault/settings/SettingsViewModel.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index eda7c4f71..1fefc9820 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -11,6 +11,7 @@ import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import android.net.Uri +import android.os.BadParcelableException import android.os.Process.myUid import android.provider.Settings import android.util.Log @@ -196,7 +197,13 @@ internal class SettingsViewModel( } private fun getAppStatusResult(): LiveData = liveData(Dispatchers.Default) { - val list = appListRetriever.getAppList() + val list = try { + Log.i(TAG, "Loading app list...") + appListRetriever.getAppList() + } catch (e: BadParcelableException) { + Log.e(TAG, "Error getting app list: ", e) + emptyList() + } val oldList = mAppStatusList.value?.appStatusList ?: emptyList() val diff = calculateDiff(AppStatusDiff(oldList, list)) emit(AppStatusResult(list, diff)) From 6e7bc89e2f98a6e8487861c3abb8c12605336de4 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 22 Feb 2024 09:15:16 -0300 Subject: [PATCH 08/76] Respect when worker was stopped and log worker ID as well as object, because we've seen two scheduled workers running at the same time, requesting a backup at the same time. This should not happen. --- .../seedvault/worker/AppBackupWorker.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt index 96ac383fe..32c44a249 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt @@ -78,13 +78,18 @@ class AppBackupWorker( private val nm: BackupNotificationManager by inject() override suspend fun doWork(): Result { + Log.i(TAG, "Start worker $this ($id)") try { setForeground(createForegroundInfo()) } catch (e: Exception) { Log.e(TAG, "Error while running setForeground: ", e) } return try { - doBackup() + if (isStopped) { + Result.retry() + } else { + doBackup() + } } finally { // schedule next backup, because the old one gets lost // when scheduling a OneTimeWorkRequest with the same unique name via scheduleNow() @@ -98,17 +103,18 @@ class AppBackupWorker( private suspend fun doBackup(): Result { var result: Result = Result.success() try { - Log.i(TAG, "Starting APK backup...") - apkBackupManager.backup() + Log.i(TAG, "Starting APK backup... (stopped: $isStopped)") + if (!isStopped) apkBackupManager.backup() } catch (e: Exception) { Log.e(TAG, "Error backing up APKs: ", e) result = Result.retry() } finally { - Log.i(TAG, "Requesting app data backup...") - val requestSuccess = if (backupRequester.isBackupEnabled) { + Log.i(TAG, "Requesting app data backup... (stopped: $isStopped)") + val requestSuccess = if (!isStopped && backupRequester.isBackupEnabled) { Log.d(TAG, "Backup is enabled, request backup...") backupRequester.requestBackup() } else true + Log.d(TAG, "Have requested backup.") if (!requestSuccess) result = Result.retry() } return result From e615402458a0c77b11d465d3f72dcbc76d2424bf Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 22 Feb 2024 09:41:53 -0300 Subject: [PATCH 09/76] During restore, show apps without APK as failed Previously, we backed up APKs of apps we could not back up (even if APK backup was disabled) so the user had a chance to get at least the apps back when restoring. Now, it is enough to record metadata about the app and the user will be able to manually install the app. The install apps step won't be skipped anymore. --- .../seedvault/restore/install/ApkRestore.kt | 14 ++++++++------ .../seedvault/worker/BackupRequester.kt | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt index 4b6ea82d8..36326e0a5 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt @@ -38,14 +38,12 @@ internal class ApkRestore( private val pm = context.packageManager - @Suppress("BlockingMethodInNonBlockingContext") fun restore(backup: RestorableBackup) = flow { - // filter out packages without APK and get total + // we don't filter out apps without APK, so the user can manually install them val packages = backup.packageMetadataMap.filter { - // We also need to exclude the DocumentsProvider used to retrieve backup data. + // We need to exclude the DocumentsProvider used to retrieve backup data. // Otherwise, it gets killed when we install it, terminating our restoration. - val isStorageProvider = it.key == storagePlugin.providerPackageName - it.value.hasApk() && !isStorageProvider + it.key != storagePlugin.providerPackageName } val total = packages.size var progress = 0 @@ -66,7 +64,11 @@ internal class ApkRestore( // re-install individual packages and emit updates for ((packageName, metadata) in packages) { try { - restore(this, backup, packageName, metadata, installResult) + if (metadata.hasApk()) { + restore(this, backup, packageName, metadata, installResult) + } else { + emit(installResult.fail(packageName)) + } } catch (e: IOException) { Log.e(TAG, "Error re-installing APK for $packageName.", e) emit(installResult.fail(packageName)) diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/BackupRequester.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/BackupRequester.kt index 9eac74069..02d4cc3bf 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/BackupRequester.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/BackupRequester.kt @@ -102,7 +102,7 @@ internal class BackupRequester( (packageIndex + NUM_PACKAGES_PER_TRANSACTION).coerceAtMost(packages.size) val packageChunk = packages.subList(packageIndex, nextChunkIndex).toTypedArray() val numBackingUp = packageIndex + packageChunk.size - Log.i(TAG, "Requesting backup for $numBackingUp/${packages.size} packages...") + Log.i(TAG, "Requesting backup for $numBackingUp of ${packages.size} packages...") packageIndex += packageChunk.size return packageChunk } From 0c1898c198f428362564109a9ae56f8036ec1e35 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 22 Feb 2024 11:37:16 -0300 Subject: [PATCH 10/76] Properly schedule/cancel backup workers when backup destination changes When the user changes to USB storage, we need to cancel current schedulings, because the storage is not always available (maybe can use a trigger URI?). And if moving to a non-USB storage, we need to schedule backups again. Unfortunately, there are two places in the code where we handle storage location changes. Ideally, those get unified at some point. --- .../seedvault/settings/SettingsFragment.kt | 11 +++--- .../seedvault/settings/SettingsViewModel.kt | 23 ++++++++---- .../ui/recoverycode/RecoveryCodeViewModel.kt | 1 + .../ui/storage/BackupStorageViewModel.kt | 35 +++++++++++++++++++ 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt index 08648a8e2..559a9c44b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -23,7 +23,6 @@ import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.restore.RestoreActivity import com.stevesoltys.seedvault.ui.toRelativeTime -import com.stevesoltys.seedvault.worker.AppBackupWorker import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.sharedViewModel @@ -128,7 +127,7 @@ class SettingsFragment : PreferenceFragmentCompat() { val disable = !(newValue as Boolean) // TODO this should really get moved out off the UI layer if (disable) { - viewModel.cancelBackupWorkers() + viewModel.cancelFilesBackup() return@OnPreferenceChangeListener true } onEnablingStorageBackup() @@ -215,10 +214,10 @@ class SettingsFragment : PreferenceFragmentCompat() { return try { backupManager.isBackupEnabled = enabled if (enabled) { - AppBackupWorker.schedule(requireContext()) + viewModel.scheduleAppBackup() viewModel.enableCallLogBackup() } else { - AppBackupWorker.unschedule(requireContext()) + viewModel.cancelAppBackup() } backup.isChecked = enabled true @@ -280,7 +279,7 @@ class SettingsFragment : PreferenceFragmentCompat() { } Long.MAX_VALUE -> { - val text = if (backupManager.isBackupEnabled) { + val text = if (backupManager.isBackupEnabled && storage?.isUsb != true) { getString(R.string.notification_title) } else { getString(R.string.settings_backup_last_backup_never) @@ -315,7 +314,7 @@ class SettingsFragment : PreferenceFragmentCompat() { LENGTH_LONG ).show() } - viewModel.scheduleBackupWorkers() + viewModel.scheduleFilesBackup() backupStorage.isChecked = true dialog.dismiss() } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index 1fefc9820..c0aedf7bf 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -129,11 +129,13 @@ internal class SettingsViewModel( Log.i(TAG, "onStorageLocationChanged") if (storage.isUsb) { // disable storage backup if new storage is on USB - cancelBackupWorkers() + cancelAppBackup() + cancelFilesBackup() } else { // enable it, just in case the previous storage was on USB, // also to update the network requirement of the new storage - scheduleBackupWorkers() + scheduleAppBackup() + scheduleFilesBackup() } onStoragePropertiesChanged() } @@ -245,11 +247,15 @@ internal class SettingsViewModel( return keyManager.hasMainKey() } - fun scheduleBackupWorkers() { + fun scheduleAppBackup() { val storage = settingsManager.getStorage() ?: error("no storage available") - if (!storage.isUsb) { - if (backupManager.isBackupEnabled) AppBackupWorker.schedule(app) - if (settingsManager.isStorageBackupEnabled()) BackupJobService.scheduleJob( + if (!storage.isUsb && backupManager.isBackupEnabled) AppBackupWorker.schedule(app) + } + + fun scheduleFilesBackup() { + val storage = settingsManager.getStorage() ?: error("no storage available") + if (!storage.isUsb && settingsManager.isStorageBackupEnabled()) { + BackupJobService.scheduleJob( context = app, jobServiceClass = StorageBackupJobService::class.java, periodMillis = HOURS.toMillis(24), @@ -261,8 +267,11 @@ internal class SettingsViewModel( } } - fun cancelBackupWorkers() { + fun cancelAppBackup() { AppBackupWorker.unschedule(app) + } + + fun cancelFilesBackup() { BackupJobService.cancelJob(app) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt index 5dbc82ef1..ba126ab1a 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt @@ -102,6 +102,7 @@ internal class RecoveryCodeViewModel( */ fun reinitializeBackupLocation() { Log.d(TAG, "Re-initializing backup location...") + // TODO this code is almost identical to BackupStorageViewModel#onLocationSet(), unify? GlobalScope.launch(Dispatchers.IO) { // remove old storage snapshots and clear cache storageBackup.deleteAllSnapshots() diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt index 33fe51b80..01cc817fb 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt @@ -4,6 +4,7 @@ import android.app.Application import android.app.backup.BackupProgress import android.app.backup.IBackupManager import android.app.backup.IBackupObserver +import android.app.job.JobInfo import android.net.Uri import android.os.UserHandle import android.util.Log @@ -11,12 +12,15 @@ import androidx.annotation.WorkerThread import androidx.lifecycle.viewModelScope import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.settings.SettingsManager +import com.stevesoltys.seedvault.storage.StorageBackupJobService import com.stevesoltys.seedvault.transport.TRANSPORT_ID import com.stevesoltys.seedvault.worker.AppBackupWorker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.calyxos.backup.storage.api.StorageBackup +import org.calyxos.backup.storage.backup.BackupJobService import java.io.IOException +import java.util.concurrent.TimeUnit private val TAG = BackupStorageViewModel::class.java.simpleName @@ -31,8 +35,18 @@ internal class BackupStorageViewModel( override fun onLocationSet(uri: Uri) { val isUsb = saveStorage(uri) + if (isUsb) { + // disable storage backup if new storage is on USB + cancelBackupWorkers() + } else { + // enable it, just in case the previous storage was on USB, + // also to update the network requirement of the new storage + scheduleBackupWorkers() + } viewModelScope.launch(Dispatchers.IO) { // remove old storage snapshots and clear cache + // TODO is this needed? It also does create all 255 chunk folders which takes time + // pass a flag to getCurrentBackupSnapshots() to not create missing folders? storageBackup.deleteAllSnapshots() storageBackup.clearCache() try { @@ -52,6 +66,27 @@ internal class BackupStorageViewModel( } } + private fun scheduleBackupWorkers() { + val storage = settingsManager.getStorage() ?: error("no storage available") + if (!storage.isUsb) { + if (backupManager.isBackupEnabled) AppBackupWorker.schedule(app) + if (settingsManager.isStorageBackupEnabled()) BackupJobService.scheduleJob( + context = app, + jobServiceClass = StorageBackupJobService::class.java, + periodMillis = TimeUnit.HOURS.toMillis(24), + networkType = if (storage.requiresNetwork) JobInfo.NETWORK_TYPE_UNMETERED + else JobInfo.NETWORK_TYPE_NONE, + deviceIdle = false, + charging = true + ) + } + } + + private fun cancelBackupWorkers() { + AppBackupWorker.unschedule(app) + BackupJobService.cancelJob(app) + } + @WorkerThread private inner class InitializationObserver(val requestBackup: Boolean) : IBackupObserver.Stub() { From 8a870d89426530e10d1a573b67d4d03ebb455ed4 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 22 Feb 2024 13:09:48 -0300 Subject: [PATCH 11/76] Use WorkInfo for determining if a backup is already running Backup and restore is not possible when a backup is running. We used to check notifications for this, but now can use WorkManager's WorkInfo which should be more reliable. Also, we used to prevent the "Backup now" action when app backup was disabled. But the user may want to do a storage backup. This is now possible. --- .../seedvault/settings/SettingsFragment.kt | 20 +++++++------ .../seedvault/settings/SettingsViewModel.kt | 28 +++++++++---------- .../stevesoltys/seedvault/storage/Services.kt | 1 + .../notification/BackupNotificationManager.kt | 10 ------- 4 files changed, 26 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt index 559a9c44b..c7375a031 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -141,10 +141,17 @@ class SettingsFragment : PreferenceFragmentCompat() { super.onViewCreated(view, savedInstanceState) viewModel.lastBackupTime.observe(viewLifecycleOwner) { time -> - setAppBackupStatusSummary(time, viewModel.nextScheduleTimeMillis.value) + setAppBackupStatusSummary( + lastBackupInMillis = time, + nextScheduleTimeMillis = viewModel.appBackupWorkInfo.value?.nextScheduleTimeMillis, + ) } - viewModel.nextScheduleTimeMillis.observe(viewLifecycleOwner) { time -> - setAppBackupStatusSummary(viewModel.lastBackupTime.value, time) + viewModel.appBackupWorkInfo.observe(viewLifecycleOwner) { workInfo -> + viewModel.onWorkerStateChanged() + setAppBackupStatusSummary( + lastBackupInMillis = viewModel.lastBackupTime.value, + nextScheduleTimeMillis = workInfo?.nextScheduleTimeMillis, + ) } val backupFiles: Preference = findPreference("backup_files")!! @@ -165,7 +172,7 @@ class SettingsFragment : PreferenceFragmentCompat() { setAutoRestoreState() setAppBackupStatusSummary( lastBackupInMillis = viewModel.lastBackupTime.value, - nextScheduleTimeMillis = viewModel.nextScheduleTimeMillis.value, + nextScheduleTimeMillis = viewModel.appBackupWorkInfo.value?.nextScheduleTimeMillis, ) } @@ -273,11 +280,6 @@ class SettingsFragment : PreferenceFragmentCompat() { if (sb.isNotEmpty()) sb.append("\n") // set time of next backup when (nextScheduleTimeMillis) { - -1L -> { - val text = getString(R.string.settings_backup_last_backup_never) - sb.append(getString(R.string.settings_backup_status_next_backup, text)) - } - Long.MAX_VALUE -> { val text = if (backupManager.isBackupEnabled && storage?.isUsb != true) { getString(R.string.notification_title) diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index c0aedf7bf..73f0bb3de 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -26,6 +26,7 @@ import androidx.lifecycle.map import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope import androidx.recyclerview.widget.DiffUtil.calculateDiff +import androidx.work.WorkInfo import androidx.work.WorkManager import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.crypto.KeyManager @@ -72,10 +73,9 @@ internal class SettingsViewModel( val backupPossible: LiveData = mBackupPossible internal val lastBackupTime = metadataManager.lastBackupTime - val nextScheduleTimeMillis = + internal val appBackupWorkInfo = workManager.getWorkInfosForUniqueWorkLiveData(UNIQUE_WORK_NAME).map { - if (it.size > 0) it[0].nextScheduleTimeMillis - else -1L + it.getOrNull(0) } private val mAppStatusList = lastBackupTime.switchMap { @@ -140,6 +140,14 @@ internal class SettingsViewModel( onStoragePropertiesChanged() } + fun onWorkerStateChanged() { + viewModelScope.launch(Dispatchers.IO) { + val canDo = settingsManager.canDoBackupNow() && + appBackupWorkInfo.value?.state != WorkInfo.State.RUNNING + mBackupPossible.postValue(canDo) + } + } + private fun onStoragePropertiesChanged() { val storage = settingsManager.getStorage() ?: return @@ -165,11 +173,8 @@ internal class SettingsViewModel( connectivityManager?.registerNetworkCallback(request, networkCallback) networkCallback.registered = true } - - viewModelScope.launch(Dispatchers.IO) { - val canDo = settingsManager.canDoBackupNow() - mBackupPossible.postValue(canDo) - } + // update whether we can do backups right now or not + onWorkerStateChanged() } override fun onCleared() { @@ -181,12 +186,7 @@ internal class SettingsViewModel( } internal fun backupNow() { - // maybe replace the check below with one that checks if our transport service is running - if (notificationManager.hasActiveBackupNotifications()) { - Toast.makeText(app, R.string.notification_backup_already_running, LENGTH_LONG).show() - } else if (!backupManager.isBackupEnabled) { - Toast.makeText(app, R.string.notification_backup_disabled, LENGTH_LONG).show() - } else viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO) { if (settingsManager.isStorageBackupEnabled()) { val i = Intent(app, StorageBackupService::class.java) // this starts an app backup afterwards diff --git a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt b/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt index e42da2d3f..59b7278e9 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt @@ -23,6 +23,7 @@ force running with: adb shell cmd jobscheduler run -f com.stevesoltys.seedvault 0 */ + internal class StorageBackupJobService : BackupJobService(StorageBackupService::class.java) internal class StorageBackupService : BackupService() { diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt index bc3fb55d7..5d100d780 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt @@ -192,16 +192,6 @@ internal class BackupNotificationManager(private val context: Context) { nm.notify(NOTIFICATION_ID_SUCCESS, notification) } - fun hasActiveBackupNotifications(): Boolean { - nm.activeNotifications.forEach { - if (it.packageName == context.packageName) { - if (it.id == NOTIFICATION_ID_BACKGROUND) return true - if (it.id == NOTIFICATION_ID_OBSERVER) return it.isOngoing - } - } - return false - } - @SuppressLint("RestrictedApi") fun onBackupError() { val intent = Intent(context, SettingsActivity::class.java) From e7e489e091e87d0d1b9662a698f0a93e69544c7a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 22 Feb 2024 13:36:43 -0300 Subject: [PATCH 12/76] Only reschedule next app backup when not on USB storage Currently, after a manual run, we need to schedule the background backups again, because the scheduling gets lost. However, we need to be careful not to do that when the backup destination is on removable storage. Then we don't want to run. --- .../java/com/stevesoltys/seedvault/UsbIntentReceiver.kt | 2 +- .../stevesoltys/seedvault/settings/SettingsViewModel.kt | 3 ++- .../java/com/stevesoltys/seedvault/storage/Services.kt | 5 ++++- .../seedvault/ui/storage/BackupStorageViewModel.kt | 3 ++- .../com/stevesoltys/seedvault/worker/AppBackupWorker.kt | 8 ++++---- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt index ff5208b03..a14cd0c65 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt @@ -63,7 +63,7 @@ class UsbIntentReceiver : UsbMonitor() { i.putExtra(EXTRA_START_APP_BACKUP, true) startForegroundService(context, i) } else { - AppBackupWorker.scheduleNow(context) + AppBackupWorker.scheduleNow(context, reschedule = false) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index 73f0bb3de..2c421e145 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -193,7 +193,8 @@ internal class SettingsViewModel( i.putExtra(EXTRA_START_APP_BACKUP, true) startForegroundService(app, i) } else { - AppBackupWorker.scheduleNow(app) + val isUsb = settingsManager.getStorage()?.isUsb ?: false + AppBackupWorker.scheduleNow(app, reschedule = !isUsb) } } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt b/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt index 59b7278e9..5a9096d30 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt @@ -1,6 +1,7 @@ package com.stevesoltys.seedvault.storage import android.content.Intent +import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.worker.AppBackupWorker import org.calyxos.backup.storage.api.BackupObserver import org.calyxos.backup.storage.api.RestoreObserver @@ -33,6 +34,7 @@ internal class StorageBackupService : BackupService() { } override val storageBackup: StorageBackup by inject() + private val settingsManager: SettingsManager by inject() // use lazy delegate because context isn't available during construction time override val backupObserver: BackupObserver by lazy { @@ -41,7 +43,8 @@ internal class StorageBackupService : BackupService() { override fun onBackupFinished(intent: Intent, success: Boolean) { if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) { - AppBackupWorker.scheduleNow(applicationContext) + val isUsb = settingsManager.getStorage()?.isUsb ?: false + AppBackupWorker.scheduleNow(applicationContext, reschedule = !isUsb) } } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt index 01cc817fb..141992ea8 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt @@ -106,7 +106,8 @@ internal class BackupStorageViewModel( // notify the UI that the location has been set mLocationChecked.postEvent(LocationResult()) if (requestBackup) { - AppBackupWorker.scheduleNow(app) + val isUsb = settingsManager.getStorage()?.isUsb ?: false + AppBackupWorker.scheduleNow(app, reschedule = !isUsb) } } else { // notify the UI that the location was invalid diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt index 32c44a249..31587fd72 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt @@ -36,7 +36,7 @@ class AppBackupWorker( companion object { private val TAG = AppBackupWorker::class.simpleName internal const val UNIQUE_WORK_NAME = "com.stevesoltys.seedvault.APP_BACKUP" - private const val TAG_NOW = "com.stevesoltys.seedvault.TAG_NOW" + private const val TAG_RESCHEDULE = "com.stevesoltys.seedvault.TAG_RESCHEDULE" fun schedule(context: Context, existingWorkPolicy: ExistingPeriodicWorkPolicy = UPDATE) { val constraints = Constraints.Builder() @@ -56,10 +56,10 @@ class AppBackupWorker( workManager.enqueueUniquePeriodicWork(UNIQUE_WORK_NAME, existingWorkPolicy, workRequest) } - fun scheduleNow(context: Context) { + fun scheduleNow(context: Context, reschedule: Boolean) { val workRequest = OneTimeWorkRequestBuilder() .setExpedited(RUN_AS_NON_EXPEDITED_WORK_REQUEST) - .addTag(TAG_NOW) + .apply { if (reschedule) addTag(TAG_RESCHEDULE) } .build() val workManager = WorkManager.getInstance(context) Log.i(TAG, "Asking to do app backup now...") @@ -93,7 +93,7 @@ class AppBackupWorker( } finally { // schedule next backup, because the old one gets lost // when scheduling a OneTimeWorkRequest with the same unique name via scheduleNow() - if (tags.contains(TAG_NOW) && backupRequester.isBackupEnabled) { + if (tags.contains(TAG_RESCHEDULE) && backupRequester.isBackupEnabled) { // needs to use CANCEL_AND_REENQUEUE otherwise it doesn't get scheduled schedule(applicationContext, CANCEL_AND_REENQUEUE) } From f593b66e00e80a75a918a78a41b14dbb23e15999 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 22 Feb 2024 13:55:24 -0300 Subject: [PATCH 13/76] try more than once to upload metadata after APK backups Failure to upload metadata after backup up APKs can be critical and flaky I/O can make it fail, so we try again. --- .../seedvault/worker/ApkBackupManager.kt | 22 +++++++++++-- .../seedvault/worker/ApkBackupManagerTest.kt | 33 +++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupManager.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupManager.kt index c75a86080..be1942c1a 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupManager.kt @@ -18,6 +18,7 @@ import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.isStopped import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.getAppName +import kotlinx.coroutines.delay import java.io.IOException import java.io.OutputStream @@ -46,9 +47,12 @@ internal class ApkBackupManager( backUpApks() } } finally { - // upload all local changes only at the end, so we don't have to re-upload the metadata - plugin.getMetadataOutputStream().use { outputStream -> - metadataManager.uploadMetadata(outputStream) + keepTrying { + // upload all local changes only at the end, + // so we don't have to re-upload the metadata + plugin.getMetadataOutputStream().use { outputStream -> + metadataManager.uploadMetadata(outputStream) + } } nm.onApkBackupDone() } @@ -109,6 +113,18 @@ internal class ApkBackupManager( } } + private suspend fun keepTrying(n: Int = 3, block: suspend () -> Unit) { + for (i in 1..n) { + try { + block() + } catch (e: Exception) { + if (i == n) throw e + Log.e(TAG, "Error (#$i), we'll keep trying", e) + delay(1000) + } + } + } + private suspend fun StoragePlugin.getMetadataOutputStream(token: Long? = null): OutputStream { val t = token ?: settingsManager.getToken() ?: throw IOException("no current token") return getOutputStream(t, FILE_BACKUP_METADATA) diff --git a/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt index 8f863d3d2..f12f4def9 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt @@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.transport.TransportTest import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import io.mockk.Runs +import io.mockk.andThenJust import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -24,6 +25,7 @@ import io.mockk.verify import io.mockk.verifyAll import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test +import java.io.IOException import java.io.OutputStream internal class ApkBackupManagerTest : TransportTest() { @@ -189,6 +191,37 @@ internal class ApkBackupManagerTest : TransportTest() { } } + @Test + fun `we keep trying to upload metadata at the end`() = runBlocking { + every { nm.onAppsNotBackedUp() } just Runs + every { packageService.notBackedUpPackages } returns listOf(packageInfo) + + every { + metadataManager.getPackageMetadata(packageInfo.packageName) + } returns packageMetadata + every { packageMetadata.state } returns UNKNOWN_ERROR + every { metadataManager.onPackageDoesNotGetBackedUp(packageInfo, NOT_ALLOWED) } just Runs + + every { settingsManager.backupApks() } returns false + + // final upload + every { settingsManager.getToken() } returns token + coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream + every { + metadataManager.uploadMetadata(metadataOutputStream) + } throws IOException() andThenThrows SecurityException() andThenJust Runs + every { metadataOutputStream.close() } just Runs + + every { nm.onApkBackupDone() } just Runs + + apkBackupManager.backup() + + verify { + metadataManager.onPackageDoesNotGetBackedUp(packageInfo, NOT_ALLOWED) + metadataOutputStream.close() + } + } + private fun expectAllAppsWillGetBackedUp() { every { nm.onAppsNotBackedUp() } just Runs every { packageService.notBackedUpPackages } returns emptyList() From 4eaa806636126c2d52d9910540836ddd8203b197 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 22 Feb 2024 16:47:01 -0300 Subject: [PATCH 14/76] Expose scheduling options in the UI --- .../java/com/stevesoltys/seedvault/App.kt | 7 +- .../seedvault/UsbIntentReceiver.kt | 10 +-- .../settings/ExpertSettingsFragment.kt | 8 +- .../seedvault/settings/SchedulingFragment.kt | 64 ++++++++++++++++ .../seedvault/settings/SettingsFragment.kt | 75 ++++++++++--------- .../seedvault/settings/SettingsManager.kt | 22 ++++++ .../seedvault/settings/SettingsViewModel.kt | 14 ++-- .../ui/storage/BackupStorageViewModel.kt | 5 +- .../seedvault/worker/AppBackupWorker.kt | 44 ++++++++--- app/src/main/res/drawable/ic_access_time.xml | 10 +++ .../res/drawable/ic_battery_charging_full.xml | 10 +++ .../main/res/drawable/ic_network_warning.xml | 10 +++ app/src/main/res/values/arrays.xml | 20 +++++ app/src/main/res/values/strings.xml | 12 +++ app/src/main/res/xml/settings.xml | 7 ++ app/src/main/res/xml/settings_scheduling.xml | 34 +++++++++ 16 files changed, 287 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/seedvault/settings/SchedulingFragment.kt create mode 100644 app/src/main/res/drawable/ic_access_time.xml create mode 100644 app/src/main/res/drawable/ic_battery_charging_full.xml create mode 100644 app/src/main/res/drawable/ic_network_warning.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/xml/settings_scheduling.xml diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index af6ac99a7..a28ac628c 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -13,6 +13,7 @@ import android.os.UserHandle import android.os.UserManager import android.provider.Settings import androidx.work.WorkManager +import androidx.work.ExistingPeriodicWorkPolicy.UPDATE import com.stevesoltys.seedvault.crypto.cryptoModule import com.stevesoltys.seedvault.header.headerModule import com.stevesoltys.seedvault.metadata.MetadataManager @@ -56,7 +57,7 @@ open class App : Application() { factory { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) } factory { AppListRetriever(this@App, get(), get(), get()) } - viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get(), get()) } + viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { BackupStorageViewModel(this@App, get(), get(), get()) } viewModel { RestoreStorageViewModel(this@App, get(), get()) } @@ -133,7 +134,9 @@ open class App : Application() { if (!isFrameworkSchedulingEnabled()) return // already on own scheduling backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false) - if (backupManager.isBackupEnabled) AppBackupWorker.schedule(applicationContext) + if (backupManager.isBackupEnabled) { + AppBackupWorker.schedule(applicationContext, settingsManager, UPDATE) + } // cancel old D2D worker WorkManager.getInstance(this).cancelUniqueWork("APP_BACKUP") } diff --git a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt index a14cd0c65..611559568 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt @@ -23,12 +23,10 @@ import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_ST import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE import com.stevesoltys.seedvault.worker.AppBackupWorker import org.koin.core.context.GlobalContext.get -import java.util.concurrent.TimeUnit.HOURS +import java.util.Date private val TAG = UsbIntentReceiver::class.java.simpleName -private const val HOURS_AUTO_BACKUP: Long = 24 - class UsbIntentReceiver : UsbMonitor() { // using KoinComponent would crash robolectric tests :( @@ -43,11 +41,13 @@ class UsbIntentReceiver : UsbMonitor() { return if (savedFlashDrive == attachedFlashDrive) { Log.d(TAG, "Matches stored device, checking backup time...") val backupMillis = System.currentTimeMillis() - metadataManager.getLastBackupTime() - if (backupMillis >= HOURS.toMillis(HOURS_AUTO_BACKUP)) { - Log.d(TAG, "Last backup older than 24 hours, requesting a backup...") + if (backupMillis >= settingsManager.backupFrequencyInMillis) { + Log.d(TAG, "Last backup older than it should be, requesting a backup...") + Log.d(TAG, " ${Date(metadataManager.getLastBackupTime())}") true } else { Log.d(TAG, "We have a recent backup, not requesting a new one.") + Log.d(TAG, " ${Date(metadataManager.getLastBackupTime())}") false } } else { diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt index c7e7d3785..269f90cbd 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt @@ -16,10 +16,10 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() { private val viewModel: SettingsViewModel by sharedViewModel() private val packageService: PackageService by inject() - // TODO set mimeType when upgrading androidx lib - private val createFileLauncher = registerForActivityResult(CreateDocument()) { uri -> - viewModel.onLogcatUriReceived(uri) - } + private val createFileLauncher = + registerForActivityResult(CreateDocument("text/plain")) { uri -> + viewModel.onLogcatUriReceived(uri) + } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { permitDiskReads { diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SchedulingFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SchedulingFragment.kt new file mode 100644 index 000000000..be3796a67 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SchedulingFragment.kt @@ -0,0 +1,64 @@ +package com.stevesoltys.seedvault.settings + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.View +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceManager +import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE +import androidx.work.ExistingPeriodicWorkPolicy.UPDATE +import com.stevesoltys.seedvault.R +import com.stevesoltys.seedvault.permitDiskReads +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.sharedViewModel + +class SchedulingFragment : PreferenceFragmentCompat(), + SharedPreferences.OnSharedPreferenceChangeListener { + + private val viewModel: SettingsViewModel by sharedViewModel() + private val settingsManager: SettingsManager by inject() + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + permitDiskReads { + setPreferencesFromResource(R.xml.settings_scheduling, rootKey) + PreferenceManager.setDefaultValues(requireContext(), R.xml.settings_scheduling, false) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val storage = settingsManager.getStorage() + if (storage?.isUsb == true) { + findPreference("scheduling_category_conditions")?.isEnabled = false + } + } + + override fun onStart() { + super.onStart() + + activity?.setTitle(R.string.settings_backup_scheduling_title) + } + + override fun onResume() { + super.onResume() + settingsManager.registerOnSharedPreferenceChangeListener(this) + } + + override fun onPause() { + super.onPause() + settingsManager.unregisterOnSharedPreferenceChangeListener(this) + } + + // we can not use setOnPreferenceChangeListener() because that gets called + // before prefs were saved + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + PREF_KEY_SCHED_FREQ -> viewModel.scheduleAppBackup(CANCEL_AND_REENQUEUE) + PREF_KEY_SCHED_METERED -> viewModel.scheduleAppBackup(UPDATE) + PREF_KEY_SCHED_CHARGING -> viewModel.scheduleAppBackup(UPDATE) + } + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt index c7375a031..33fe15dcd 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -19,12 +19,15 @@ import androidx.preference.Preference import androidx.preference.Preference.OnPreferenceChangeListener import androidx.preference.PreferenceFragmentCompat import androidx.preference.TwoStatePreference +import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE +import androidx.work.WorkInfo import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.restore.RestoreActivity import com.stevesoltys.seedvault.ui.toRelativeTime import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import java.util.concurrent.TimeUnit private val TAG = SettingsFragment::class.java.name @@ -39,6 +42,7 @@ class SettingsFragment : PreferenceFragmentCompat() { private lateinit var apkBackup: TwoStatePreference private lateinit var backupLocation: Preference private lateinit var backupStatus: Preference + private lateinit var backupScheduling: Preference private lateinit var backupStorage: TwoStatePreference private lateinit var backupRecoveryCode: Preference @@ -121,6 +125,7 @@ class SettingsFragment : PreferenceFragmentCompat() { return@OnPreferenceChangeListener false } backupStatus = findPreference("backup_status")!! + backupScheduling = findPreference("backup_scheduling")!! backupStorage = findPreference("backup_storage")!! backupStorage.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> @@ -141,17 +146,11 @@ class SettingsFragment : PreferenceFragmentCompat() { super.onViewCreated(view, savedInstanceState) viewModel.lastBackupTime.observe(viewLifecycleOwner) { time -> - setAppBackupStatusSummary( - lastBackupInMillis = time, - nextScheduleTimeMillis = viewModel.appBackupWorkInfo.value?.nextScheduleTimeMillis, - ) + setAppBackupStatusSummary(time) } viewModel.appBackupWorkInfo.observe(viewLifecycleOwner) { workInfo -> viewModel.onWorkerStateChanged() - setAppBackupStatusSummary( - lastBackupInMillis = viewModel.lastBackupTime.value, - nextScheduleTimeMillis = workInfo?.nextScheduleTimeMillis, - ) + setAppBackupSchedulingSummary(workInfo) } val backupFiles: Preference = findPreference("backup_files")!! @@ -170,10 +169,8 @@ class SettingsFragment : PreferenceFragmentCompat() { setBackupEnabledState() setBackupLocationSummary() setAutoRestoreState() - setAppBackupStatusSummary( - lastBackupInMillis = viewModel.lastBackupTime.value, - nextScheduleTimeMillis = viewModel.appBackupWorkInfo.value?.nextScheduleTimeMillis, - ) + setAppBackupStatusSummary(viewModel.lastBackupTime.value) + setAppBackupSchedulingSummary(viewModel.appBackupWorkInfo.value) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -221,7 +218,7 @@ class SettingsFragment : PreferenceFragmentCompat() { return try { backupManager.isBackupEnabled = enabled if (enabled) { - viewModel.scheduleAppBackup() + viewModel.scheduleAppBackup(CANCEL_AND_REENQUEUE) viewModel.enableCallLogBackup() } else { viewModel.cancelAppBackup() @@ -265,37 +262,41 @@ class SettingsFragment : PreferenceFragmentCompat() { backupLocation.summary = storage?.name ?: getString(R.string.settings_backup_location_none) } - private fun setAppBackupStatusSummary( - lastBackupInMillis: Long?, - nextScheduleTimeMillis: Long?, - ) { - val sb = StringBuilder() + private fun setAppBackupStatusSummary(lastBackupInMillis: Long?) { if (lastBackupInMillis != null) { // set time of last backup val lastBackup = lastBackupInMillis.toRelativeTime(requireContext()) - sb.append(getString(R.string.settings_backup_status_summary, lastBackup)) + backupStatus.summary = getString(R.string.settings_backup_status_summary, lastBackup) } - if (nextScheduleTimeMillis != null) { - // insert linebreak, if we have text before - if (sb.isNotEmpty()) sb.append("\n") - // set time of next backup - when (nextScheduleTimeMillis) { - Long.MAX_VALUE -> { - val text = if (backupManager.isBackupEnabled && storage?.isUsb != true) { - getString(R.string.notification_title) - } else { - getString(R.string.settings_backup_last_backup_never) - } - sb.append(getString(R.string.settings_backup_status_next_backup, text)) - } + } - else -> { - val text = nextScheduleTimeMillis.toRelativeTime(requireContext()) - sb.append(getString(R.string.settings_backup_status_next_backup_estimate, text)) - } + private fun setAppBackupSchedulingSummary(workInfo: WorkInfo?) { + if (storage?.isUsb == true) { + backupScheduling.summary = getString(R.string.settings_backup_status_next_backup_usb) + return + } + if (workInfo == null) return + + val nextScheduleTimeMillis = workInfo.nextScheduleTimeMillis + if (workInfo.state == WorkInfo.State.RUNNING) { + val text = getString(R.string.notification_title) + backupScheduling.summary = getString(R.string.settings_backup_status_next_backup, text) + } else if (nextScheduleTimeMillis == Long.MAX_VALUE) { + val text = getString(R.string.settings_backup_last_backup_never) + backupScheduling.summary = getString(R.string.settings_backup_status_next_backup, text) + } else { + val diff = System.currentTimeMillis() - nextScheduleTimeMillis + val isPast = diff > TimeUnit.MINUTES.toMillis(1) + if (isPast) { + val text = getString(R.string.settings_backup_status_next_backup_past) + backupScheduling.summary = + getString(R.string.settings_backup_status_next_backup, text) + } else { + val text = nextScheduleTimeMillis.toRelativeTime(requireContext()) + backupScheduling.summary = + getString(R.string.settings_backup_status_next_backup_estimate, text) } } - backupStatus.summary = sb.toString() } private fun onEnablingStorageBackup() { diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt index 47176a091..5990dcb18 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -1,6 +1,7 @@ package com.stevesoltys.seedvault.settings import android.content.Context +import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.hardware.usb.UsbDevice import android.net.ConnectivityManager import android.net.NetworkCapabilities @@ -17,6 +18,9 @@ import java.util.concurrent.ConcurrentSkipListSet internal const val PREF_KEY_TOKEN = "token" internal const val PREF_KEY_BACKUP_APK = "backup_apk" internal const val PREF_KEY_AUTO_RESTORE = "auto_restore" +internal const val PREF_KEY_SCHED_FREQ = "scheduling_frequency" +internal const val PREF_KEY_SCHED_METERED = "scheduling_metered" +internal const val PREF_KEY_SCHED_CHARGING = "scheduling_charging" private const val PREF_KEY_STORAGE_URI = "storageUri" private const val PREF_KEY_STORAGE_NAME = "storageName" @@ -43,6 +47,14 @@ class SettingsManager(private val context: Context) { @Volatile private var token: Long? = null + fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) { + prefs.registerOnSharedPreferenceChangeListener(listener) + } + + fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) { + prefs.unregisterOnSharedPreferenceChangeListener(listener) + } + /** * This gets accessed by non-UI threads when saving with [PreferenceManager] * and when [isBackupEnabled] is called during a backup run. @@ -141,6 +153,16 @@ class SettingsManager(private val context: Context) { return prefs.getBoolean(PREF_KEY_BACKUP_APK, true) } + val backupFrequencyInMillis: Long + get() { + return prefs.getString(PREF_KEY_SCHED_FREQ, "86400000")?.toLongOrNull() + ?: 86400000 // 24h + } + val useMeteredNetwork: Boolean + get() = prefs.getBoolean(PREF_KEY_SCHED_METERED, false) + val backupOnlyWhenCharging: Boolean + get() = prefs.getBoolean(PREF_KEY_SCHED_CHARGING, true) + fun isBackupEnabled(packageName: String) = !blacklistedApps.contains(packageName) fun isStorageBackupEnabled() = prefs.getBoolean(PREF_KEY_BACKUP_STORAGE, false) diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index 2c421e145..b80ed3312 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -26,6 +26,8 @@ import androidx.lifecycle.map import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope import androidx.recyclerview.widget.DiffUtil.calculateDiff +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE import androidx.work.WorkInfo import androidx.work.WorkManager import com.stevesoltys.seedvault.R @@ -36,7 +38,6 @@ import com.stevesoltys.seedvault.storage.StorageBackupJobService import com.stevesoltys.seedvault.storage.StorageBackupService import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel -import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.worker.AppBackupWorker import com.stevesoltys.seedvault.worker.AppBackupWorker.Companion.UNIQUE_WORK_NAME import kotlinx.coroutines.Dispatchers @@ -55,7 +56,6 @@ internal class SettingsViewModel( app: Application, settingsManager: SettingsManager, keyManager: KeyManager, - private val notificationManager: BackupNotificationManager, private val metadataManager: MetadataManager, private val appListRetriever: AppListRetriever, private val storageBackup: StorageBackup, @@ -126,7 +126,7 @@ internal class SettingsViewModel( override fun onStorageLocationChanged() { val storage = settingsManager.getStorage() ?: return - Log.i(TAG, "onStorageLocationChanged") + Log.i(TAG, "onStorageLocationChanged (isUsb: ${storage.isUsb}") if (storage.isUsb) { // disable storage backup if new storage is on USB cancelAppBackup() @@ -134,7 +134,7 @@ internal class SettingsViewModel( } else { // enable it, just in case the previous storage was on USB, // also to update the network requirement of the new storage - scheduleAppBackup() + scheduleAppBackup(CANCEL_AND_REENQUEUE) scheduleFilesBackup() } onStoragePropertiesChanged() @@ -248,9 +248,11 @@ internal class SettingsViewModel( return keyManager.hasMainKey() } - fun scheduleAppBackup() { + fun scheduleAppBackup(existingWorkPolicy: ExistingPeriodicWorkPolicy) { val storage = settingsManager.getStorage() ?: error("no storage available") - if (!storage.isUsb && backupManager.isBackupEnabled) AppBackupWorker.schedule(app) + if (!storage.isUsb && backupManager.isBackupEnabled) { + AppBackupWorker.schedule(app, settingsManager, existingWorkPolicy) + } } fun scheduleFilesBackup() { diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt index 141992ea8..7a3a06a6f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt @@ -10,6 +10,7 @@ import android.os.UserHandle import android.util.Log import androidx.annotation.WorkerThread import androidx.lifecycle.viewModelScope +import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.storage.StorageBackupJobService @@ -69,7 +70,9 @@ internal class BackupStorageViewModel( private fun scheduleBackupWorkers() { val storage = settingsManager.getStorage() ?: error("no storage available") if (!storage.isUsb) { - if (backupManager.isBackupEnabled) AppBackupWorker.schedule(app) + if (backupManager.isBackupEnabled) { + AppBackupWorker.schedule(app, settingsManager, CANCEL_AND_REENQUEUE) + } if (settingsManager.isStorageBackupEnabled()) BackupJobService.scheduleJob( context = app, jobServiceClass = StorageBackupJobService::class.java, diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt index 31587fd72..64a1cbed8 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt @@ -7,13 +7,13 @@ package com.stevesoltys.seedvault.worker import android.content.Context import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC +import android.text.format.DateUtils.formatElapsedTime import android.util.Log import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.CoroutineWorker import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE -import androidx.work.ExistingPeriodicWorkPolicy.UPDATE import androidx.work.ExistingWorkPolicy.REPLACE import androidx.work.ForegroundInfo import androidx.work.NetworkType @@ -22,6 +22,7 @@ import androidx.work.OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters +import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.NOTIFICATION_ID_OBSERVER import org.koin.core.component.KoinComponent @@ -38,21 +39,43 @@ class AppBackupWorker( internal const val UNIQUE_WORK_NAME = "com.stevesoltys.seedvault.APP_BACKUP" private const val TAG_RESCHEDULE = "com.stevesoltys.seedvault.TAG_RESCHEDULE" - fun schedule(context: Context, existingWorkPolicy: ExistingPeriodicWorkPolicy = UPDATE) { - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.UNMETERED) - .setRequiresCharging(true) - .build() + /** + * (Re-)schedules the [AppBackupWorker]. + * + * @param existingWorkPolicy usually you want to use [ExistingPeriodicWorkPolicy.UPDATE] + * only if you are sure that work is still scheduled + * and you don't want to mess with the scheduling time. + * In most other cases, you want to use [ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE], + * because it ensures work gets schedules, even if it wasn't scheduled before. + * It will however reset the scheduling time. + */ + fun schedule( + context: Context, + settingsManager: SettingsManager, + existingWorkPolicy: ExistingPeriodicWorkPolicy, + ) { + val logFrequency = formatElapsedTime(settingsManager.backupFrequencyInMillis / 1000) + Log.i(TAG, "Scheduling in $logFrequency...") + val constraints = Constraints.Builder().apply { + if (!settingsManager.useMeteredNetwork) { + Log.i(TAG, " only on unmetered networks") + setRequiredNetworkType(NetworkType.UNMETERED) + } + if (settingsManager.backupOnlyWhenCharging) { + Log.i(TAG, " only when the device is charging") + setRequiresCharging(true) + } + }.build() val workRequest = PeriodicWorkRequestBuilder( - repeatInterval = 24, - repeatIntervalTimeUnit = TimeUnit.HOURS, + repeatInterval = settingsManager.backupFrequencyInMillis, + repeatIntervalTimeUnit = TimeUnit.MILLISECONDS, flexTimeInterval = 2, flexTimeIntervalUnit = TimeUnit.HOURS, ).setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES) .build() val workManager = WorkManager.getInstance(context) - Log.i(TAG, "Scheduling app backup: $workRequest") + Log.i(TAG, " workRequest: ${workRequest.id}") workManager.enqueueUniquePeriodicWork(UNIQUE_WORK_NAME, existingWorkPolicy, workRequest) } @@ -74,6 +97,7 @@ class AppBackupWorker( } private val backupRequester: BackupRequester by inject() + private val settingsManager: SettingsManager by inject() private val apkBackupManager: ApkBackupManager by inject() private val nm: BackupNotificationManager by inject() @@ -95,7 +119,7 @@ class AppBackupWorker( // when scheduling a OneTimeWorkRequest with the same unique name via scheduleNow() if (tags.contains(TAG_RESCHEDULE) && backupRequester.isBackupEnabled) { // needs to use CANCEL_AND_REENQUEUE otherwise it doesn't get scheduled - schedule(applicationContext, CANCEL_AND_REENQUEUE) + schedule(applicationContext, settingsManager, CANCEL_AND_REENQUEUE) } } } diff --git a/app/src/main/res/drawable/ic_access_time.xml b/app/src/main/res/drawable/ic_access_time.xml new file mode 100644 index 000000000..2b1853f25 --- /dev/null +++ b/app/src/main/res/drawable/ic_access_time.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_battery_charging_full.xml b/app/src/main/res/drawable/ic_battery_charging_full.xml new file mode 100644 index 000000000..92496d40e --- /dev/null +++ b/app/src/main/res/drawable/ic_battery_charging_full.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_network_warning.xml b/app/src/main/res/drawable/ic_network_warning.xml new file mode 100644 index 000000000..a24198096 --- /dev/null +++ b/app/src/main/res/drawable/ic_network_warning.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 000000000..aaa635565 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,20 @@ + + + + + @string/settings_scheduling_frequency_12_hours + @string/settings_scheduling_frequency_daily + @string/settings_scheduling_frequency_3_days + @string/settings_scheduling_frequency_weekly + + + + 43200000 + 86400000 + 259200000 + 604800000 + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13c094361..a961a078f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,6 +32,9 @@ Last backup: %1$s Next backup: %1$s Next backup (estimate): %1$s + once conditions are fulfilled + Backups will happen automatically when you plug in your USB drive + Backup scheduling Exclude apps Backup now Storage backup (beta) @@ -48,6 +51,15 @@ To continue using app backups, you need to generate a new recovery code.\n\nWe are sorry for the inconvenience. New code + Backup frequency + Every 12 hours + Daily + Every 3 days + Weekly + Conditions + Back up when using mobile data + Back up only when charging + Expert settings Unlimited app quota Do not impose a limitation on the size of app backups.\n\nWarning: This can fill up your storage location quickly. Not needed for most apps. diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index e9034e67a..784f04009 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -46,6 +46,13 @@ app:summary="@string/settings_backup_apk_summary" app:title="@string/settings_backup_apk_title" /> + + diff --git a/app/src/main/res/xml/settings_scheduling.xml b/app/src/main/res/xml/settings_scheduling.xml new file mode 100644 index 000000000..d5bccf1e4 --- /dev/null +++ b/app/src/main/res/xml/settings_scheduling.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + From 19bfc41d95144375dd52ab926c3baade2f4f24ed Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 23 Feb 2024 11:35:26 -0300 Subject: [PATCH 15/76] Allow backups in metered network, if user wants that --- .../seedvault/settings/SettingsManager.kt | 12 +++++++----- .../seedvault/transport/backup/BackupCoordinator.kt | 5 ++++- .../transport/backup/BackupCoordinatorTest.kt | 2 ++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt index 5990dcb18..786f5781c 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -146,7 +146,8 @@ class SettingsManager(private val context: Context) { fun canDoBackupNow(): Boolean { val storage = getStorage() ?: return false val systemContext = context.getStorageContext { storage.isUsb } - return !storage.isUnavailableUsb(systemContext) && !storage.isUnavailableNetwork(context) + return !storage.isUnavailableUsb(systemContext) && + !storage.isUnavailableNetwork(context, useMeteredNetwork) } fun backupApks(): Boolean { @@ -208,15 +209,16 @@ data class Storage( * Returns true if this is storage that requires network access, * but it isn't available right now. */ - fun isUnavailableNetwork(context: Context): Boolean { - return requiresNetwork && !hasUnmeteredInternet(context) + fun isUnavailableNetwork(context: Context, allowMetered: Boolean): Boolean { + return requiresNetwork && !hasUnmeteredInternet(context, allowMetered) } - private fun hasUnmeteredInternet(context: Context): Boolean { + private fun hasUnmeteredInternet(context: Context, allowMetered: Boolean): Boolean { val cm = context.getSystemService(ConnectivityManager::class.java) ?: return false val isMetered = cm.isActiveNetworkMetered val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false - return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && !isMetered + return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + (allowMetered || !isMetered) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt index 1dfbbce0f..e9275bf7e 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt @@ -422,7 +422,10 @@ internal class BackupCoordinator( // back off if storage is removable and not available right now storage.isUnavailableUsb(context) -> longBackoff // back off if storage is on network, but we have no access - storage.isUnavailableNetwork(context) -> HOURS.toMillis(1) + storage.isUnavailableNetwork( + context = context, + allowMetered = settingsManager.useMeteredNetwork, + ) -> HOURS.toMillis(1) // otherwise no back off else -> 0L } diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt index a883aaa40..2fed54abe 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt @@ -294,6 +294,7 @@ internal class BackupCoordinatorTest : BackupTest() { } just Runs coEvery { full.cancelFullBackup(token, metadata.salt, false) } just Runs every { settingsManager.getStorage() } returns storage + every { settingsManager.useMeteredNetwork } returns false every { metadataOutputStream.close() } just Runs assertEquals( @@ -343,6 +344,7 @@ internal class BackupCoordinatorTest : BackupTest() { } just Runs coEvery { full.cancelFullBackup(token, metadata.salt, false) } just Runs every { settingsManager.getStorage() } returns storage + every { settingsManager.useMeteredNetwork } returns false every { metadataOutputStream.close() } just Runs assertEquals( From aa1c7106241b403aa41344e2f4e3c15f3ab2b05c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 26 Feb 2024 10:48:56 -0300 Subject: [PATCH 16/76] Stop running instrumentation tests for SDK 33 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf8aa1b8b..ae649ce71 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - android_target: [ 33, 34 ] + android_target: [ 34 ] emulator_type: [ aosp_atd ] d2d_backup_test: [ true, false ] steps: From 23787a373e07c17c0298414afff9486a38142b0d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 26 Feb 2024 11:22:34 -0300 Subject: [PATCH 17/76] Don't retry instrumentation tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ae649ce71..372144ca6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: - name: Run tests uses: Wandalen/wretry.action@v1.3.0 with: - attempt_limit: 3 + attempt_limit: 1 action: reactivecircus/android-emulator-runner@v2 with: | api-level: ${{ matrix.android_target }} From 2da989971bf1d6efc67ef4958736087a7b10fa70 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 26 Feb 2024 16:07:16 -0300 Subject: [PATCH 18/76] Request @pm@ backup after initialization to avoid a 2nd restore set being used. This also changes the initialization behavior to only create the restore set folder and upload the metadata only when we actually need to. This way, double inits are not creating new restore sets on the backup destination. --- .../seedvault/KoinInstrumentationTestApp.kt | 2 +- .../java/com/stevesoltys/seedvault/App.kt | 2 +- .../seedvault/metadata/MetadataManager.kt | 9 ++- .../saf/DocumentsProviderStoragePlugin.kt | 9 --- .../transport/backup/BackupCoordinator.kt | 26 +++---- .../transport/backup/BackupInitializer.kt | 73 +++++++++++++++++++ .../transport/backup/BackupModule.kt | 1 + .../ui/recoverycode/RecoveryCodeViewModel.kt | 14 ++-- .../ui/storage/BackupStorageViewModel.kt | 60 +++++---------- .../seedvault/metadata/MetadataManagerTest.kt | 2 +- .../transport/backup/BackupCoordinatorTest.kt | 7 +- 11 files changed, 118 insertions(+), 87 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupInitializer.kt diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt index 6a2e56013..c00438f20 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt @@ -47,7 +47,7 @@ class KoinInstrumentationTestApp : App() { viewModel { currentBackupStorageViewModel = - spyk(BackupStorageViewModel(context, get(), get(), get())) + spyk(BackupStorageViewModel(context, get(), get(), get(), get())) currentBackupStorageViewModel!! } diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index a28ac628c..9391a801e 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -59,7 +59,7 @@ open class App : Application() { viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) } - viewModel { BackupStorageViewModel(this@App, get(), get(), get()) } + viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get()) } viewModel { RestoreStorageViewModel(this@App, get(), get()) } viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { FileSelectionViewModel(this@App, get()) } 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 5c3d1a7a6..bd5b4f381 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt @@ -59,14 +59,15 @@ internal class MetadataManager( /** * Call this when initializing a new device. * - * Existing [BackupMetadata] will be cleared, use the given new token, - * and written encrypted to the given [OutputStream] as well as the internal cache. + * Existing [BackupMetadata] will be cleared + * and new metadata with the given [token] will be written to the internal cache + * with a fresh salt. */ @Synchronized @Throws(IOException::class) - fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) { + fun onDeviceInitialization(token: Long) { val salt = crypto.getRandomBytes(METADATA_SALT_SIZE).encodeBase64() - modifyMetadata(metadataOutputStream) { + modifyCachedMetadata { metadata = BackupMetadata(token = token, salt = salt) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt index 0dbc6c50e..e8e02baa5 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt @@ -35,22 +35,13 @@ internal class DocumentsProviderStoragePlugin( override suspend fun startNewRestoreSet(token: Long) { // reset current storage storage.reset(token) - - // get or create root backup dir - storage.rootBackupDir ?: throw IOException() } @Throws(IOException::class) override suspend fun initializeDevice() { - // wipe existing data - storage.getSetDir()?.deleteContents(context) - // reset storage without new token, so folders get recreated // otherwise stale DocumentFiles will hang around storage.reset(null) - - // create backup folders - storage.currentSetDir ?: throw IOException() } @Throws(IOException::class) diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt index e9275bf7e..5846c4aa8 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt @@ -86,12 +86,13 @@ internal class BackupCoordinator( * @return the token of the new [RestoreSet]. */ @Throws(IOException::class) - private suspend fun startNewRestoreSet(): Long { + private suspend fun startNewRestoreSet() { val token = clock.time() Log.i(TAG, "Starting new RestoreSet with token $token...") settingsManager.setNewToken(token) plugin.startNewRestoreSet(token) - return token + Log.d(TAG, "Resetting backup metadata...") + metadataManager.onDeviceInitialization(token) } /** @@ -115,18 +116,14 @@ internal class BackupCoordinator( suspend fun initializeDevice(): Int = try { // we don't respect the intended system behavior here by always starting a new [RestoreSet] // instead of simply deleting the current one - val token = startNewRestoreSet() + startNewRestoreSet() Log.i(TAG, "Initialize Device!") plugin.initializeDevice() - Log.d(TAG, "Resetting backup metadata for token $token...") - plugin.getMetadataOutputStream(token).use { - metadataManager.onDeviceInitialization(token, it) - } // [finishBackup] will only be called when we return [TRANSPORT_OK] here // so we remember that we initialized successfully state.calledInitialize = true TRANSPORT_OK - } catch (e: IOException) { + } catch (e: Exception) { Log.e(TAG, "Error initializing device", e) // Show error notification if we needed init or were ready for backups if (metadataManager.requiresInit || settingsManager.canDoBackupNow()) nm.onBackupError() @@ -222,14 +219,11 @@ internal class BackupCoordinator( state.cancelReason = UNKNOWN_ERROR if (metadataManager.requiresInit) { Log.w(TAG, "Metadata requires re-init!") - // start a new restore set to upgrade from legacy format - // by starting a clean backup with all files using the new version - try { - startNewRestoreSet() - } catch (e: IOException) { - Log.e(TAG, "Error starting new restore set", e) - } - // this causes a backup error, but things should go back to normal afterwards + // Tell the system that we are not initialized, it will initialize us afterwards. + // This will start a new restore set to upgrade from legacy format + // by starting a clean backup with all files using the new version. + // + // This causes a backup error, but things should go back to normal afterwards. return TRANSPORT_NOT_INITIALIZED } val token = settingsManager.getToken() ?: error("no token in performFullBackup") diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupInitializer.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupInitializer.kt new file mode 100644 index 000000000..9d83d6185 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupInitializer.kt @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.transport.backup + +import android.app.backup.BackupProgress +import android.app.backup.IBackupManager +import android.app.backup.IBackupObserver +import android.os.UserHandle +import android.util.Log +import androidx.annotation.WorkerThread +import com.stevesoltys.seedvault.BackupMonitor +import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER +import com.stevesoltys.seedvault.transport.TRANSPORT_ID + +class BackupInitializer( + private val backupManager: IBackupManager, +) { + + companion object { + private val TAG = BackupInitializer::class.simpleName + } + + fun initialize(onError: () -> Unit, onSuccess: () -> Unit) { + val observer = BackupObserver("Initialization", onError) { + // After successful initialization, we request a @pm@ backup right away, + // because if this finds empty state, it asks us to do another initialization. + // And then we end up with yet another restore set token. + // Since we want the final token as soon as possible, we need to get it here. + Log.d(TAG, "Requesting initial $MAGIC_PACKAGE_MANAGER backup...") + backupManager.requestBackup( + arrayOf(MAGIC_PACKAGE_MANAGER), + BackupObserver("Initial backup of @pm@", onError, onSuccess), + BackupMonitor(), + 0, + ) + } + backupManager.initializeTransportsForUser( + UserHandle.myUserId(), + arrayOf(TRANSPORT_ID), + observer, + ) + } + + @WorkerThread + private inner class BackupObserver( + private val operation: String, + private val onError: () -> Unit, + private val onSuccess: () -> Unit, + ) : IBackupObserver.Stub() { + override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) { + // noop + } + + override fun onResult(target: String, status: Int) { + // noop + } + + override fun backupFinished(status: Int) { + if (Log.isLoggable(TAG, Log.INFO)) { + Log.i(TAG, "$operation finished. Status: $status") + } + if (status == 0) { + onSuccess() + } else { + onError() + } + } + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt index 3ed9caedd..6bb6f6e2c 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt @@ -4,6 +4,7 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val backupModule = module { + single { BackupInitializer(get()) } single { InputFactory() } single { PackageService( diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt index ba126ab1a..274187c3b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt @@ -1,7 +1,6 @@ package com.stevesoltys.seedvault.ui.recoverycode import android.app.backup.IBackupManager -import android.os.UserHandle import android.util.Log import androidx.lifecycle.AndroidViewModel import cash.z.ecc.android.bip39.Mnemonics @@ -12,8 +11,7 @@ import cash.z.ecc.android.bip39.toSeed import com.stevesoltys.seedvault.App import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.KeyManager -import com.stevesoltys.seedvault.transport.TRANSPORT_ID -import com.stevesoltys.seedvault.transport.backup.BackupCoordinator +import com.stevesoltys.seedvault.transport.backup.BackupInitializer import com.stevesoltys.seedvault.ui.LiveEvent import com.stevesoltys.seedvault.ui.MutableLiveEvent import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager @@ -32,7 +30,7 @@ internal class RecoveryCodeViewModel( private val crypto: Crypto, private val keyManager: KeyManager, private val backupManager: IBackupManager, - private val backupCoordinator: BackupCoordinator, + private val backupInitializer: BackupInitializer, private val notificationManager: BackupNotificationManager, private val storageBackup: StorageBackup, ) : AndroidViewModel(app) { @@ -109,11 +107,9 @@ internal class RecoveryCodeViewModel( storageBackup.clearCache() try { // initialize the new location - if (backupManager.isBackupEnabled) backupManager.initializeTransportsForUser( - UserHandle.myUserId(), - arrayOf(TRANSPORT_ID), - null - ) + if (backupManager.isBackupEnabled) backupInitializer.initialize({ }) { + // no-op + } } catch (e: IOException) { Log.e(TAG, "Error starting new RestoreSet", e) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt index 7a3a06a6f..bf6c729bb 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt @@ -1,20 +1,16 @@ package com.stevesoltys.seedvault.ui.storage import android.app.Application -import android.app.backup.BackupProgress import android.app.backup.IBackupManager -import android.app.backup.IBackupObserver import android.app.job.JobInfo import android.net.Uri -import android.os.UserHandle import android.util.Log -import androidx.annotation.WorkerThread import androidx.lifecycle.viewModelScope import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.storage.StorageBackupJobService -import com.stevesoltys.seedvault.transport.TRANSPORT_ID +import com.stevesoltys.seedvault.transport.backup.BackupInitializer import com.stevesoltys.seedvault.worker.AppBackupWorker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -28,6 +24,7 @@ private val TAG = BackupStorageViewModel::class.java.simpleName internal class BackupStorageViewModel( private val app: Application, private val backupManager: IBackupManager, + private val backupInitializer: BackupInitializer, private val storageBackup: StorageBackup, settingsManager: SettingsManager, ) : StorageViewModel(app, settingsManager) { @@ -52,13 +49,23 @@ internal class BackupStorageViewModel( storageBackup.clearCache() try { // initialize the new location (if backups are enabled) - if (backupManager.isBackupEnabled) backupManager.initializeTransportsForUser( - UserHandle.myUserId(), - arrayOf(TRANSPORT_ID), - // if storage is on USB and this is not SetupWizard, do a backup right away - InitializationObserver(isUsb && !isSetupWizard) - ) else { - InitializationObserver(false).backupFinished(0) + if (backupManager.isBackupEnabled) { + val onError = { + Log.e(TAG, "Error starting new RestoreSet") + onInitializationError() + } + backupInitializer.initialize(onError) { + val requestBackup = isUsb && !isSetupWizard + if (requestBackup) { + Log.i(TAG, "Requesting a backup now, because we use USB storage") + AppBackupWorker.scheduleNow(app, reschedule = false) + } + // notify the UI that the location has been set + mLocationChecked.postEvent(LocationResult()) + } + } else { + // notify the UI that the location has been set + mLocationChecked.postEvent(LocationResult()) } } catch (e: IOException) { Log.e(TAG, "Error starting new RestoreSet", e) @@ -90,35 +97,6 @@ internal class BackupStorageViewModel( BackupJobService.cancelJob(app) } - @WorkerThread - private inner class InitializationObserver(val requestBackup: Boolean) : - IBackupObserver.Stub() { - override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) { - // noop - } - - override fun onResult(target: String, status: Int) { - // noop - } - - override fun backupFinished(status: Int) { - if (Log.isLoggable(TAG, Log.INFO)) { - Log.i(TAG, "Initialization finished. Status: $status") - } - if (status == 0) { - // notify the UI that the location has been set - mLocationChecked.postEvent(LocationResult()) - if (requestBackup) { - val isUsb = settingsManager.getStorage()?.isUsb ?: false - AppBackupWorker.scheduleNow(app, reschedule = !isUsb) - } - } else { - // notify the UI that the location was invalid - onInitializationError() - } - } - } - private fun onInitializationError() { val errorMsg = app.getString(R.string.storage_check_fragment_backup_error) mLocationChecked.postEvent(LocationResult(errorMsg)) 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 f3dbb0167..62300e76d 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt @@ -100,7 +100,7 @@ class MetadataManagerTest { expectReadFromCache() expectModifyMetadata(initialMetadata) - manager.onDeviceInitialization(token, storageOutputStream) + manager.onDeviceInitialization(token) assertEquals(token, manager.getBackupToken()) assertEquals(0L, manager.getLastBackupTime()) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt index 2fed54abe..598a6ff43 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt @@ -69,22 +69,18 @@ internal class BackupCoordinatorTest : BackupTest() { fun `device initialization succeeds and delegates to plugin`() = runBlocking { expectStartNewRestoreSet() coEvery { plugin.initializeDevice() } just Runs - coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream - every { metadataManager.onDeviceInitialization(token, metadataOutputStream) } just Runs every { kv.hasState() } returns false every { full.hasState() } returns false - every { metadataOutputStream.close() } just Runs assertEquals(TRANSPORT_OK, backup.initializeDevice()) assertEquals(TRANSPORT_OK, backup.finishBackup()) - - verify { metadataOutputStream.close() } } private suspend fun expectStartNewRestoreSet() { every { clock.time() } returns token every { settingsManager.setNewToken(token) } just Runs coEvery { plugin.startNewRestoreSet(token) } just Runs + every { metadataManager.onDeviceInitialization(token) } just Runs } @Test @@ -136,6 +132,7 @@ internal class BackupCoordinatorTest : BackupTest() { every { clock.time() } returns token + 1 every { settingsManager.setNewToken(token + 1) } just Runs coEvery { plugin.startNewRestoreSet(token + 1) } just Runs + every { metadataManager.onDeviceInitialization(token + 1) } just Runs every { data.close() } just Runs From ee581ee652e38a61ca44a26a76954cf565d1eb19 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 27 Feb 2024 14:51:02 -0300 Subject: [PATCH 19/76] Initialize backup, when enabling it For ApkBackup, we need to be initialized. If the system starts with app backup off, we would not initialize which would lead to issues when backing up the APKs. --- .../seedvault/e2e/LargeBackupTestBase.kt | 6 ++++ .../java/com/stevesoltys/seedvault/App.kt | 2 +- .../seedvault/settings/SettingsActivity.kt | 14 +++++++++ .../seedvault/settings/SettingsFragment.kt | 9 +----- .../seedvault/settings/SettingsViewModel.kt | 31 +++++++++++++++++++ .../seedvault/ui/BackupActivity.kt | 12 ++++--- 6 files changed, 60 insertions(+), 14 deletions(-) diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt index 3a49d35ec..09b88e3f6 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt @@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.e2e import android.content.pm.PackageInfo import android.os.ParcelFileDescriptor +import androidx.test.uiautomator.Until import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept import com.stevesoltys.seedvault.e2e.io.InputStreamIntercept import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen @@ -44,6 +45,11 @@ internal interface LargeBackupTestBase : LargeTestBase { if (!backupManager.isBackupEnabled) { backupSwitch.click() waitUntilIdle() + + BackupScreen { + device.wait(Until.hasObject(initializingText), 10000) + device.wait(Until.gone(initializingText), 120000) + } } backupMenu.clickAndWaitForNewWindow() diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index 9391a801e..a51b5263d 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -57,7 +57,7 @@ open class App : Application() { factory { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) } factory { AppListRetriever(this@App, get(), get(), get()) } - viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get()) } + viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get(), get()) } viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get()) } viewModel { RestoreStorageViewModel(this@App, get(), get()) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt index 11fc6b5ea..3aecb911d 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt @@ -10,6 +10,7 @@ import com.stevesoltys.seedvault.ui.RequireProvisioningActivity import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.recoverycode.ARG_FOR_NEW_CODE +import com.stevesoltys.seedvault.ui.storage.StorageCheckFragment import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel @@ -36,6 +37,19 @@ class SettingsActivity : RequireProvisioningActivity(), OnPreferenceStartFragmen if (intent?.action == ACTION_APP_STATUS_LIST) { showFragment(AppStatusFragment(), true) } + + // observe initialization and show/remove init fragment + // this can happen when enabling backup and storage wasn't initialized + viewModel.initEvent.observeEvent(this) { show -> + val tag = "INIT" + if (show) { + val title = getString(R.string.storage_check_fragment_backup_title) + showFragment(StorageCheckFragment.newInstance(title), true, tag) + } else { + val f = supportFragmentManager.findFragmentByTag(tag) + if (f != null && f.isVisible) supportFragmentManager.popBackStack() + } + } } @CallSuper diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt index 33fe15dcd..d93d573d2 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -19,7 +19,6 @@ import androidx.preference.Preference import androidx.preference.Preference.OnPreferenceChangeListener import androidx.preference.PreferenceFragmentCompat import androidx.preference.TwoStatePreference -import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE import androidx.work.WorkInfo import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.permitDiskReads @@ -213,16 +212,10 @@ class SettingsFragment : PreferenceFragmentCompat() { else -> super.onOptionsItemSelected(item) } - // TODO this should really get moved out off the UI layer private fun trySetBackupEnabled(enabled: Boolean): Boolean { return try { backupManager.isBackupEnabled = enabled - if (enabled) { - viewModel.scheduleAppBackup(CANCEL_AND_REENQUEUE) - viewModel.enableCallLogBackup() - } else { - viewModel.cancelAppBackup() - } + viewModel.onBackupEnabled(enabled) backup.isChecked = enabled true } catch (e: RemoteException) { diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index b80ed3312..993101100 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -37,6 +37,9 @@ import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.storage.StorageBackupJobService import com.stevesoltys.seedvault.storage.StorageBackupService import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP +import com.stevesoltys.seedvault.transport.backup.BackupInitializer +import com.stevesoltys.seedvault.ui.LiveEvent +import com.stevesoltys.seedvault.ui.MutableLiveEvent import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.worker.AppBackupWorker import com.stevesoltys.seedvault.worker.AppBackupWorker.Companion.UNIQUE_WORK_NAME @@ -60,6 +63,7 @@ internal class SettingsViewModel( private val appListRetriever: AppListRetriever, private val storageBackup: StorageBackup, private val backupManager: IBackupManager, + private val backupInitializer: BackupInitializer, ) : RequireProvisioningViewModel(app, settingsManager, keyManager) { private val contentResolver = app.contentResolver @@ -90,6 +94,9 @@ internal class SettingsViewModel( private val _filesSummary = MutableLiveData() internal val filesSummary: LiveData = _filesSummary + private val _initEvent = MutableLiveEvent() + val initEvent: LiveEvent = _initEvent + private val storageObserver = object : ContentObserver(null) { override fun onChange(selfChange: Boolean, uris: MutableCollection, flags: Int) { onStoragePropertiesChanged() @@ -230,6 +237,30 @@ internal class SettingsViewModel( } } + fun onBackupEnabled(enabled: Boolean) { + if (enabled) { + if (metadataManager.requiresInit) { + val onError: () -> Unit = { + viewModelScope.launch(Dispatchers.Main) { + val res = R.string.storage_check_fragment_backup_error + Toast.makeText(app, res, LENGTH_LONG).show() + } + } + viewModelScope.launch(Dispatchers.IO) { + backupInitializer.initialize(onError) { + _initEvent.postEvent(false) + scheduleAppBackup(CANCEL_AND_REENQUEUE) + } + _initEvent.postEvent(true) + } + } + // enable call log backups for existing installs (added end of 2020) + enableCallLogBackup() + } else { + cancelAppBackup() + } + } + /** * Ensures that the call log will be included in backups. * diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivity.kt index 40d35fba4..7bb8b6b6a 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivity.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivity.kt @@ -17,11 +17,13 @@ abstract class BackupActivity : AppCompatActivity() { else -> super.onOptionsItemSelected(item) } - protected fun showFragment(f: Fragment, addToBackStack: Boolean = false) { - val fragmentTransaction = supportFragmentManager.beginTransaction() - .replace(R.id.fragment, f) - if (addToBackStack) fragmentTransaction.addToBackStack(null) - fragmentTransaction.commit() + protected fun showFragment(f: Fragment, addToBackStack: Boolean = false, tag: String? = null) { + supportFragmentManager.beginTransaction().apply { + if (tag == null) replace(R.id.fragment, f) + else replace(R.id.fragment, f, tag) + if (addToBackStack) addToBackStack(null) + commit() + } } } From 8489753d58e9cbc07f8b19e5ea919d4d14cf3bb8 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 6 Mar 2024 14:05:52 -0300 Subject: [PATCH 20/76] Address review feedback --- .../seedvault/metadata/MetadataManager.kt | 35 ++++++++----------- .../settings/ExpertSettingsFragment.kt | 3 +- .../seedvault/settings/SettingsActivity.kt | 4 +-- .../seedvault/settings/SettingsFragment.kt | 7 ++++ 4 files changed, 25 insertions(+), 24 deletions(-) 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 bd5b4f381..b97c85efb 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt @@ -125,21 +125,20 @@ internal class MetadataManager( val now = clock.time() metadata.time = now metadata.d2dBackup = settingsManager.d2dBackupsEnabled() - - if (metadata.packageMetadataMap.containsKey(packageName)) { - metadata.packageMetadataMap[packageName]!!.time = now - metadata.packageMetadataMap[packageName]!!.state = APK_AND_DATA - metadata.packageMetadataMap[packageName]!!.backupType = type - // don't override a previous K/V size, if there were no K/V changes - if (size != null) metadata.packageMetadataMap[packageName]!!.size = size - } else { - metadata.packageMetadataMap[packageName] = PackageMetadata( + metadata.packageMetadataMap.getOrPut(packageName) { + PackageMetadata( time = now, state = APK_AND_DATA, backupType = type, size = size, system = packageInfo.isSystemApp(), ) + }.apply { + time = now + state = APK_AND_DATA + backupType = type + // don't override a previous K/V size, if there were no K/V changes + if (size != null) this.size = size } } } @@ -159,18 +158,15 @@ internal class MetadataManager( backupType: BackupType? = null, ) { check(packageState != APK_AND_DATA) { "Backup Error with non-error package state." } - val packageName = packageInfo.packageName modifyMetadata(metadataOutputStream) { - if (metadata.packageMetadataMap.containsKey(packageName)) { - metadata.packageMetadataMap[packageName]!!.state = packageState - } else { - metadata.packageMetadataMap[packageName] = PackageMetadata( + metadata.packageMetadataMap.getOrPut(packageInfo.packageName) { + PackageMetadata( time = 0L, state = packageState, backupType = backupType, system = packageInfo.isSystemApp() ) - } + }.state = packageState } } @@ -186,16 +182,13 @@ internal class MetadataManager( packageInfo: PackageInfo, packageState: PackageState, ) = modifyCachedMetadata { - val packageName = packageInfo.packageName - if (metadata.packageMetadataMap.containsKey(packageName)) { - metadata.packageMetadataMap[packageName]!!.state = packageState - } else { - metadata.packageMetadataMap[packageName] = PackageMetadata( + metadata.packageMetadataMap.getOrPut(packageInfo.packageName) { + PackageMetadata( time = 0L, state = packageState, system = packageInfo.isSystemApp(), ) - } + }.state = packageState } /** diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt index 269f90cbd..27a822ed9 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt @@ -5,6 +5,7 @@ import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat +import com.google.android.mms.ContentType.TEXT_PLAIN import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.transport.backup.PackageService @@ -17,7 +18,7 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() { private val packageService: PackageService by inject() private val createFileLauncher = - registerForActivityResult(CreateDocument("text/plain")) { uri -> + registerForActivityResult(CreateDocument(TEXT_PLAIN)) { uri -> viewModel.onLogcatUriReceived(uri) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt index 3aecb911d..0e224c655 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt @@ -46,8 +46,8 @@ class SettingsActivity : RequireProvisioningActivity(), OnPreferenceStartFragmen val title = getString(R.string.storage_check_fragment_backup_title) showFragment(StorageCheckFragment.newInstance(title), true, tag) } else { - val f = supportFragmentManager.findFragmentByTag(tag) - if (f != null && f.isVisible) supportFragmentManager.popBackStack() + val fragment = supportFragmentManager.findFragmentByTag(tag) + if (fragment?.isVisible == true) supportFragmentManager.popBackStack() } } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt index d93d573d2..e76bcf823 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -263,6 +263,13 @@ class SettingsFragment : PreferenceFragmentCompat() { } } + /** + * Sets the summary for scheduling which is information about when the next backup is scheduled. + * + * It could be that it shows the backup as running, + * gives an estimate about when the next run will be or + * says that nothing is scheduled which can happen when backup destination is on flash drive. + */ private fun setAppBackupSchedulingSummary(workInfo: WorkInfo?) { if (storage?.isUsb == true) { backupScheduling.summary = getString(R.string.settings_backup_status_next_backup_usb) From 0b021e3b48a236210d82763cebf779cac4259629 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 6 Mar 2024 16:15:01 -0300 Subject: [PATCH 21/76] Try to close system dialogs of emulator --- app/development/scripts/provision_emulator.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/development/scripts/provision_emulator.sh b/app/development/scripts/provision_emulator.sh index 284e70829..030eff7af 100755 --- a/app/development/scripts/provision_emulator.sh +++ b/app/development/scripts/provision_emulator.sh @@ -97,4 +97,7 @@ $ADB shell mkdir -p /sdcard/seedvault_baseline $ADB shell tar xzf /sdcard/backup.tar.gz --directory=/sdcard/seedvault_baseline $ADB shell rm /sdcard/backup.tar.gz +# sometimes a system dialog (e.g. launcher stopped) is showing and taking focus +$ADB shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS + echo "Emulator '$EMULATOR_NAME' has been provisioned with Seedvault!" From 6e4bf90e7c6a3e68b908e1f0790bd014e49027ef Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Thu, 7 Mar 2024 15:46:39 +0530 Subject: [PATCH 22/76] BackupScreen: Don't hardcode model name for internal storage Signed-off-by: Aayush Gupta --- .../com/stevesoltys/seedvault/e2e/screen/impl/BackupScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/BackupScreen.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/BackupScreen.kt index 581512df8..ff87985b3 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/BackupScreen.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/BackupScreen.kt @@ -1,5 +1,6 @@ package com.stevesoltys.seedvault.e2e.screen.impl +import android.os.Build import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen @@ -16,7 +17,7 @@ object BackupScreen : UiDeviceScreen() { val backupSwitch = findObject { text("Backup my apps") } - val internalStorageButton = findObject { textContains("Android SDK built for") } + val internalStorageButton = findObject { textContains(Build.MODEL) } val useAnywayButton = findObject { text("USE ANYWAY") } From f7730d3034560d52de8c79479d44ae9ca0913d90 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 13 Mar 2024 09:55:46 -0300 Subject: [PATCH 23/76] Report total number of user apps when showing final notification Before, we showed the number of apps we requested the backup for which in case of non-d2d may be much lower than the number of installed apps. In the future we may decide to also include certain system apps in that count. --- .../ui/notification/NotificationBackupObserver.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt index 5f5eaea4f..41498d89f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt @@ -10,6 +10,7 @@ import android.util.Log.isLoggable import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.metadata.MetadataManager +import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.worker.BackupRequester import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -24,6 +25,7 @@ internal class NotificationBackupObserver( private val nm: BackupNotificationManager by inject() private val metadataManager: MetadataManager by inject() + private val packageService: PackageService by inject() private var currentPackage: String? = null private var numPackages: Int = 0 private var pmCounted: Boolean = false @@ -81,7 +83,13 @@ internal class NotificationBackupObserver( val success = status == 0 val numBackedUp = if (success) metadataManager.getPackagesNumBackedUp() else null val size = if (success) metadataManager.getPackagesBackupSize() else 0L - nm.onBackupFinished(success, numBackedUp, requestedPackages, size) + val total = try { + packageService.allUserPackages.size + } catch (e: Exception) { + Log.e(TAG, "Error getting number of all user packages: ", e) + requestedPackages + } + nm.onBackupFinished(success, numBackedUp, total, size) } } From baef15b2bc6a1858f1065461b925e19321ab5138 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 19 Mar 2024 11:07:55 -0300 Subject: [PATCH 24/76] Do live-counting of backed up apps for success notification Previously, we asked the MetadataManager which also includes historic data and may provide misleading totals. --- .../seedvault/metadata/MetadataManager.kt | 13 ------- .../NotificationBackupObserver.kt | 34 +++++++++++++------ 2 files changed, 23 insertions(+), 24 deletions(-) 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 b97c85efb..6c09ac8f8 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt @@ -14,7 +14,6 @@ import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA -import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.backup.isSystemApp import java.io.FileNotFoundException @@ -268,18 +267,6 @@ internal class MetadataManager( return metadata.packageMetadataMap[packageName]?.copy() } - @Synchronized - fun getPackagesNumBackedUp(): Int { - // FIXME we are under-reporting packages here, - // because we have no way to also include upgraded system apps - return metadata.packageMetadataMap.filter { (_, packageMetadata) -> - !packageMetadata.system && ( // ignore system apps - packageMetadata.state == APK_AND_DATA || // either full success - packageMetadata.state == NO_DATA // or apps that simply had no data - ) - }.count() - } - @Synchronized fun getPackagesBackupSize(): Long { return metadata.packageMetadataMap.values.sumOf { it.size ?: 0L } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt index 41498d89f..30d934414 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt @@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.ui.notification import android.app.backup.BackupProgress import android.app.backup.IBackupObserver import android.content.Context +import android.content.pm.ApplicationInfo.FLAG_SYSTEM import android.content.pm.PackageManager.NameNotFoundException import android.util.Log import android.util.Log.INFO @@ -28,6 +29,7 @@ internal class NotificationBackupObserver( private val packageService: PackageService by inject() private var currentPackage: String? = null private var numPackages: Int = 0 + private var numPackagesToReport: Int = 0 private var pmCounted: Boolean = false init { @@ -64,6 +66,26 @@ internal class NotificationBackupObserver( if (isLoggable(TAG, INFO)) { Log.i(TAG, "Completed. Target: $target, status: $status") } + // prevent double counting of @pm@ which gets backed up with each requested chunk + if (target == MAGIC_PACKAGE_MANAGER) { + if (!pmCounted) { + numPackages += 1 + pmCounted = true + } + } else { + numPackages += 1 + } + // count package if success and not a system app + if (status == 0 && target != null && target != MAGIC_PACKAGE_MANAGER) try { + val appInfo = context.packageManager.getApplicationInfo(target, 0) + // exclude system apps from final count for now + if (appInfo.flags and FLAG_SYSTEM == 0) { + numPackagesToReport += 1 + } + } catch (e: Exception) { + // should only happen for MAGIC_PACKAGE_MANAGER, but better save than sorry + Log.e(TAG, "Error getting ApplicationInfo: ", e) + } // often [onResult] gets called right away without any [onUpdate] call showProgressNotification(target) } @@ -81,7 +103,6 @@ internal class NotificationBackupObserver( Log.i(TAG, "Backup finished $numPackages/$requestedPackages. Status: $status") } val success = status == 0 - val numBackedUp = if (success) metadataManager.getPackagesNumBackedUp() else null val size = if (success) metadataManager.getPackagesBackupSize() else 0L val total = try { packageService.allUserPackages.size @@ -89,7 +110,7 @@ internal class NotificationBackupObserver( Log.e(TAG, "Error getting number of all user packages: ", e) requestedPackages } - nm.onBackupFinished(success, numBackedUp, total, size) + nm.onBackupFinished(success, numPackagesToReport, total, size) } } @@ -107,15 +128,6 @@ internal class NotificationBackupObserver( } else { context.getString(R.string.backup_section_system) } - // prevent double counting of @pm@ which gets backed up with each requested chunk - if (packageName == MAGIC_PACKAGE_MANAGER) { - if (!pmCounted) { - numPackages += 1 - pmCounted = true - } - } else { - numPackages += 1 - } Log.i(TAG, "$numPackages/$requestedPackages - $appName ($packageName)") nm.onBackupUpdate(name, numPackages, requestedPackages) } From fef6ecc640cd74a57bf8d522f7fcd06d2ef9b91f Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 8 Mar 2024 10:36:18 -0300 Subject: [PATCH 25/76] Fix flakyness in SmallFileBackupIntegrationTest --- .../storage/backup/SmallFileBackupIntegrationTest.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupIntegrationTest.kt b/storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupIntegrationTest.kt index 4d8214e41..ce2a4a772 100644 --- a/storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupIntegrationTest.kt +++ b/storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupIntegrationTest.kt @@ -77,7 +77,6 @@ internal class SmallFileBackupIntegrationTest { val outputStream2 = ByteArrayOutputStream() val chunkId = Random.nextBytes(KEY_SIZE_BYTES) - val cachedChunk = CachedChunk(chunkId.toHexString(), 0, 181, 0) val cachedFile2 = file2.toCachedFile(listOf(chunkId.toHexString()), 1) val backupFile = file2.toBackupFile(cachedFile2.chunks, cachedFile2.zipIndex) @@ -93,7 +92,14 @@ internal class SmallFileBackupIntegrationTest { every { mac.doFinal(any()) } returns chunkId every { chunksCache.get(any()) } returns null every { storagePlugin.getChunkOutputStream(any()) } returns outputStream2 - every { chunksCache.insert(cachedChunk) } just Runs + every { + chunksCache.insert(match { cachedChunk -> + cachedChunk.id == chunkId.toHexString() && + cachedChunk.refCount == 0L && + cachedChunk.size <= outputStream2.size() && + cachedChunk.version == 0.toByte() + }) + } just Runs every { filesCache.upsert(match { it.copy(lastSeen = cachedFile2.lastSeen) == cachedFile2 From 9557dfd4e763b8738086f0c39a2d3014e6be8315 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 5 Apr 2024 08:45:52 -0300 Subject: [PATCH 26/76] Fix flakyness in SmallFileBackupIntegrationTest again Since the chunk gets zipped, the random input data would sometimes differ in size if randomness wasn't truly random, so it could be compressed more. --- .../backup/storage/backup/SmallFileBackupIntegrationTest.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupIntegrationTest.kt b/storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupIntegrationTest.kt index ce2a4a772..31815b865 100644 --- a/storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupIntegrationTest.kt +++ b/storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupIntegrationTest.kt @@ -105,7 +105,9 @@ internal class SmallFileBackupIntegrationTest { it.copy(lastSeen = cachedFile2.lastSeen) == cachedFile2 }) } just Runs - coEvery { observer.onFileBackedUp(file2, true, 0, 181, "S") } just Runs + coEvery { + observer.onFileBackedUp(file2, true, 0, match { it <= outputStream2.size() }, "S") + } just Runs val result = smallFileBackup.backupFiles(files, availableChunkIds, observer) assertEquals(setOf(chunkId.toHexString()), result.chunkIds) @@ -114,7 +116,7 @@ internal class SmallFileBackupIntegrationTest { assertEquals(0, result.backupMediaFiles.size) coVerify { - observer.onFileBackedUp(file2, true, 0, 181, "S") + observer.onFileBackedUp(file2, true, 0, match { it <= outputStream2.size() }, "S") } } From 6e63d9bac039010dfc0dc13c0969fb033df7cae1 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 11 Apr 2024 17:11:41 -0300 Subject: [PATCH 27/76] Update build badge in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e6fa5f960..3f97ac35d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Seedvault -[![Build](https://github.com/seedvault-app/seedvault/workflows/Build/badge.svg?branch=master)](https://github.com/seedvault-app/seedvault/actions?query=branch%3Amaster+workflow%3ABuild) +[![Build](https://github.com/seedvault-app/seedvault/actions/workflows/build.yml/badge.svg)](https://github.com/seedvault-app/seedvault/actions/workflows/build.yml) A backup application for the [Android Open Source Project](https://source.android.com/). From 87db20e45f3452f8c1b048e619e0d9b17682050b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 12 Feb 2024 12:01:38 -0300 Subject: [PATCH 28/76] Include user profile name in backup name so it is easier to identify the right backup if more users backup to the same storage medium. Change-Id: I56fa93899da3764e20b6aee40adfd52722a05a9f --- README.md | 1 + app/src/main/AndroidManifest.xml | 4 ++ .../seedvault/metadata/MetadataManager.kt | 22 ++++++++++- .../seedvault/metadata/MetadataManagerTest.kt | 39 ++++++++++++++++++- permissions_com.stevesoltys.seedvault.xml | 1 + 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f97ac35d..b396a5a53 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ It uses the same internal APIs as `adb backup` which is deprecated and thus need * `android.permission.MANAGE_USB` to access the serial number of USB mass storage devices. * `android.permission.WRITE_SECURE_SETTINGS` to change system backup settings and enable call log backup. * `android.permission.QUERY_ALL_PACKAGES` to get information about all installed apps for backup. +* `android.permission.QUERY_USERS` to get the name of the user profile that gets backed up. * `android.permission.INSTALL_PACKAGES` to re-install apps when restoring from backup. * `android.permission.MANAGE_EXTERNAL_STORAGE` to backup and restore files from device storage. * `android.permission.ACCESS_MEDIA_LOCATION` to backup original media files e.g. without stripped EXIF metadata. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c3bd194cc..f22788148 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -64,6 +64,10 @@ android:name="android.permission.READ_LOGS" tools:ignore="ProtectedPermissions" /> + + + 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 6c09ac8f8..2346adb6b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt @@ -3,6 +3,9 @@ package com.stevesoltys.seedvault.metadata import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.pm.PackageInfo +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.os.Build +import android.os.UserManager import android.util.Log import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread @@ -67,7 +70,16 @@ internal class MetadataManager( fun onDeviceInitialization(token: Long) { val salt = crypto.getRandomBytes(METADATA_SALT_SIZE).encodeBase64() modifyCachedMetadata { - metadata = BackupMetadata(token = token, salt = salt) + val userName = getUserName() + metadata = BackupMetadata( + token = token, + salt = salt, + deviceName = if (userName == null) { + "${Build.MANUFACTURER} ${Build.MODEL}" + } else { + "${Build.MANUFACTURER} ${Build.MODEL} - $userName" + }, + ) } } @@ -297,4 +309,12 @@ internal class MetadataManager( } } + private fun getUserName(): String? { + val perm = "android.permission.QUERY_USERS" + return if (context.checkSelfPermission(perm) == PERMISSION_GRANTED) { + val userManager = context.getSystemService(UserManager::class.java) + userManager.userName + } else null + } + } 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 62300e76d..308f9105a 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt @@ -6,6 +6,8 @@ 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.os.UserManager import androidx.test.ext.junit.runners.AndroidJUnit4 import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.TestApp @@ -94,12 +96,46 @@ class MetadataManagerTest { } @Test - fun `test onDeviceInitialization()`() { + fun `test onDeviceInitialization() without user permission`() { every { clock.time() } returns time every { crypto.getRandomBytes(METADATA_SALT_SIZE) } returns saltBytes expectReadFromCache() expectModifyMetadata(initialMetadata) + every { + context.checkSelfPermission("android.permission.QUERY_USERS") + } returns PackageManager.PERMISSION_DENIED + + manager.onDeviceInitialization(token) + + assertEquals(token, manager.getBackupToken()) + assertEquals(0L, manager.getLastBackupTime()) + + verify { + cacheInputStream.close() + cacheOutputStream.close() + } + } + + @Test + fun `test onDeviceInitialization() with user permission`() { + val userManager: UserManager = mockk() + val userName = getRandomString() + val newMetadata = initialMetadata.copy( + deviceName = initialMetadata.deviceName + " - $userName", + ) + + every { clock.time() } returns time + every { crypto.getRandomBytes(METADATA_SALT_SIZE) } returns saltBytes + expectReadFromCache() + expectModifyMetadata(newMetadata) + + every { + context.checkSelfPermission("android.permission.QUERY_USERS") + } returns PackageManager.PERMISSION_GRANTED + every { context.getSystemService(UserManager::class.java) } returns userManager + every { userManager.userName } returns userName + manager.onDeviceInitialization(token) assertEquals(token, manager.getBackupToken()) @@ -108,6 +144,7 @@ class MetadataManagerTest { verify { cacheInputStream.close() cacheOutputStream.close() + userManager.userName } } diff --git a/permissions_com.stevesoltys.seedvault.xml b/permissions_com.stevesoltys.seedvault.xml index cb85ca60a..f6b63de33 100644 --- a/permissions_com.stevesoltys.seedvault.xml +++ b/permissions_com.stevesoltys.seedvault.xml @@ -5,6 +5,7 @@ + From 499126c4597e06a06b5ed4acb684053b0ba5bb3f Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 14 Mar 2024 09:00:44 -0300 Subject: [PATCH 29/76] Log pre-flight errors in BackupMonitor --- .../java/com/stevesoltys/seedvault/BackupMonitor.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/BackupMonitor.kt b/app/src/main/java/com/stevesoltys/seedvault/BackupMonitor.kt index 8e9619167..b2b73b465 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/BackupMonitor.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/BackupMonitor.kt @@ -3,6 +3,8 @@ package com.stevesoltys.seedvault import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_ID import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_NAME +import android.app.backup.BackupManagerMonitor.EXTRA_LOG_PREFLIGHT_ERROR +import android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT import android.app.backup.IBackupManagerMonitor import android.os.Bundle import android.util.Log @@ -13,10 +15,16 @@ private val TAG = BackupMonitor::class.java.name class BackupMonitor : IBackupManagerMonitor.Stub() { override fun onEvent(bundle: Bundle) { + val id = bundle.getInt(EXTRA_LOG_EVENT_ID) + val packageName = bundle.getString(EXTRA_LOG_EVENT_PACKAGE_NAME, "?") + if (id == LOG_EVENT_ID_ERROR_PREFLIGHT) { + val preflightResult = bundle.getLong(EXTRA_LOG_PREFLIGHT_ERROR, -1) + Log.w(TAG, "Pre-flight error from $packageName: $preflightResult") + } if (!Log.isLoggable(TAG, DEBUG)) return - Log.d(TAG, "ID: " + bundle.getInt(EXTRA_LOG_EVENT_ID)) + Log.d(TAG, "ID: $id") Log.d(TAG, "CATEGORY: " + bundle.getInt(EXTRA_LOG_EVENT_CATEGORY, -1)) - Log.d(TAG, "PACKAGE: " + bundle.getString(EXTRA_LOG_EVENT_PACKAGE_NAME, "?")) + Log.d(TAG, "PACKAGE: $packageName") } } From c8d21fcf344af990aeb465e3774f2f2e3046414b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 14 Mar 2024 11:14:31 -0300 Subject: [PATCH 30/76] Auto-disable apps that cancel the entire backup This can happen when the app process gets killed while its BackupAgent is running. There are several qcom apps in the wild that have this issue. These are DoSing our backups and are non-free, so we are defending ourselves against them. --- .../java/com/stevesoltys/seedvault/App.kt | 4 ++- .../seedvault/settings/SettingsManager.kt | 9 +++++++ .../NotificationBackupObserver.kt | 26 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index a51b5263d..372c86fcb 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -2,6 +2,7 @@ package com.stevesoltys.seedvault import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL import android.app.Application +import android.app.backup.BackupManager import android.app.backup.BackupManager.PACKAGE_MANAGER_SENTINEL import android.app.backup.IBackupManager import android.content.Context @@ -147,9 +148,10 @@ open class App : Application() { } -const val MAGIC_PACKAGE_MANAGER = PACKAGE_MANAGER_SENTINEL +const val MAGIC_PACKAGE_MANAGER: String = PACKAGE_MANAGER_SENTINEL const val ANCESTRAL_RECORD_KEY = "@ancestral_record@" const val GLOBAL_METADATA_KEY = "@meta@" +const val ERROR_BACKUP_CANCELLED: Int = BackupManager.ERROR_BACKUP_CANCELLED // TODO this doesn't work for LineageOS as they do public debug builds fun isDebugBuild() = Build.TYPE == "userdebug" diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt index 786f5781c..9584ffead 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -166,6 +166,15 @@ class SettingsManager(private val context: Context) { fun isBackupEnabled(packageName: String) = !blacklistedApps.contains(packageName) + /** + * Disables backup for an app. Similar to [onAppBackupStatusChanged]. + */ + fun disableBackup(packageName: String) { + if (blacklistedApps.add(packageName)) { + prefs.edit().putStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, blacklistedApps).apply() + } + } + fun isStorageBackupEnabled() = prefs.getBoolean(PREF_KEY_BACKUP_STORAGE, false) @UiThread diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt index 30d934414..c507bd59f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt @@ -1,6 +1,7 @@ package com.stevesoltys.seedvault.ui.notification import android.app.backup.BackupProgress +import android.app.backup.BackupTransport.AGENT_ERROR import android.app.backup.IBackupObserver import android.content.Context import android.content.pm.ApplicationInfo.FLAG_SYSTEM @@ -8,9 +9,11 @@ import android.content.pm.PackageManager.NameNotFoundException import android.util.Log import android.util.Log.INFO import android.util.Log.isLoggable +import com.stevesoltys.seedvault.ERROR_BACKUP_CANCELLED import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.metadata.MetadataManager +import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.worker.BackupRequester import org.koin.core.component.KoinComponent @@ -27,11 +30,14 @@ internal class NotificationBackupObserver( private val nm: BackupNotificationManager by inject() private val metadataManager: MetadataManager by inject() private val packageService: PackageService by inject() + private val settingsManager: SettingsManager by inject() private var currentPackage: String? = null private var numPackages: Int = 0 private var numPackagesToReport: Int = 0 private var pmCounted: Boolean = false + private var errorPackageName: String? = null + init { // Inform the notification manager that a backup has started // and inform about the expected numbers of apps. @@ -86,6 +92,17 @@ internal class NotificationBackupObserver( // should only happen for MAGIC_PACKAGE_MANAGER, but better save than sorry Log.e(TAG, "Error getting ApplicationInfo: ", e) } + + // Apps that get killed while interacting with their [BackupAgent] cancel the entire backup. + // In order to prevent them from DoSing us, we remember them here to auto-disable them. + // We noticed that the same app behavior can cause a status of + // either AGENT_ERROR or ERROR_BACKUP_CANCELLED, so we need to handle both. + errorPackageName = if (status == AGENT_ERROR || status == ERROR_BACKUP_CANCELLED) { + target + } else { + null // To not disable apps by mistake, we reset it when getting a new non-error result. + } + // often [onResult] gets called right away without any [onUpdate] call showProgressNotification(target) } @@ -98,6 +115,15 @@ internal class NotificationBackupObserver( * as a whole failed. */ override fun backupFinished(status: Int) { + if (status == ERROR_BACKUP_CANCELLED) { + val packageName = errorPackageName + if (packageName == null) { + Log.e(TAG, "Backup got cancelled, but there we have no culprit :(") + } else { + Log.w(TAG, "App $packageName misbehaved, will disable backup for it...") + settingsManager.disableBackup(packageName) + } + } if (backupRequester.requestNext()) { if (isLoggable(TAG, INFO)) { Log.i(TAG, "Backup finished $numPackages/$requestedPackages. Status: $status") From 11fbd450da9b9f783dce1b856fd9d83d71121f51 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 16 Apr 2024 15:42:29 -0300 Subject: [PATCH 31/76] Fix nullability for UserManager in AOSP build (#646) --- .../java/com/stevesoltys/seedvault/metadata/MetadataManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2346adb6b..55b297327 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt @@ -312,7 +312,7 @@ internal class MetadataManager( private fun getUserName(): String? { val perm = "android.permission.QUERY_USERS" return if (context.checkSelfPermission(perm) == PERMISSION_GRANTED) { - val userManager = context.getSystemService(UserManager::class.java) + val userManager = context.getSystemService(UserManager::class.java) ?: return null userManager.userName } else null } From a25ecd188655ff7e88116fd8593057ab418311cd Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 24 Apr 2024 07:31:58 +0200 Subject: [PATCH 32/76] Import translations from Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Anonymous Co-authored-by: Fjuro Co-authored-by: Fjuro Co-authored-by: Hosted Weblate Co-authored-by: Igor Sorocean Co-authored-by: Kristian Nordin Co-authored-by: Michael Bestas Co-authored-by: Oğuz Ersen Co-authored-by: Suguru Hirahara Co-authored-by: Yaron Shahrabani Co-authored-by: Yuya Co-authored-by: ZehRique Co-authored-by: gallegonovato Co-authored-by: lucasmz Co-authored-by: lucasmz Co-authored-by: lucasmz Co-authored-by: nautilusx Co-authored-by: rehork Co-authored-by: 大王叫我来巡山 Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/cs/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/de/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/el/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/es/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/fi/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/he/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/it/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/ja/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/pl/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/pt/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/ro/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/sv/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/tr/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault_contactsbackup/pt/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault_contactsbackup/ro/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault_storage/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault_storage/ro/ Translate-URL: https://hosted.weblate.org/projects/calyxos/seedvault_storage/sv/ Translation: CalyxOS/Seedvault Translation: CalyxOS/Seedvault contacts backup Translation: CalyxOS/Seedvault storage backup --- app/src/main/res/values-cs/strings.xml | 26 ++- app/src/main/res/values-de/strings.xml | 3 + app/src/main/res/values-el/strings.xml | 26 ++- app/src/main/res/values-es/strings.xml | 26 ++- app/src/main/res/values-fi/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-iw/strings.xml | 4 + app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 30 ++- app/src/main/res/values-pt-rBR/strings.xml | 217 ++++++++++-------- app/src/main/res/values-pt/strings.xml | 45 +++- app/src/main/res/values-ro/strings.xml | 187 ++++++++++++++- app/src/main/res/values-sv/strings.xml | 4 + app/src/main/res/values-tr/strings.xml | 26 ++- app/src/main/res/values-zh-rCN/strings.xml | 26 ++- .../src/main/res/values-pt/strings.xml | 4 +- .../src/main/res/values-ro/strings.xml | 4 +- .../src/main/res/values-pt-rBR/strings.xml | 10 +- .../lib/src/main/res/values-ro/strings.xml | 22 +- .../lib/src/main/res/values-sv/strings.xml | 14 +- 20 files changed, 548 insertions(+), 132 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 9913bb468..f047a3efe 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -142,7 +142,7 @@ Stav a nastavení zálohy Záloha Seedvault Záloha - Záloha úložiště (experimentální) + Záloha úložiště (beta) Zahrnuté soubory a složky Vaše soubory jsou obnovovány na pozadí. Během tohoto procesu můžete začít používat telefon. \n @@ -153,9 +153,9 @@ Žádné Odborné nastavení Neomezená kvóta aplikací - Experimentální funkce + Beta funkce Přesto zapnout - Zálohování souborů je stále experimentální a nemusí fungovat. Nespoléhejte se na něj, pokud jde o důležitá data. + Zálohování souborů je ve fázi beta a nemusí fungovat. Nespoléhejte se na něj, pokud jde o důležitá data. Neomezuje velikost záloh aplikací. \n \nUpozornění: Toto může rychle zaplnit vaše úložiště. Pro většinu aplikací není potřeba. @@ -163,7 +163,7 @@ Připojení WebDAV není k dispozici. Nastavte ho. Přeskočit obnovení aplikací Zadejte znovu zámek obrazovky - Vyberte zálohu úložiště pro obnovení (experimentální) + Vyberte zálohu úložiště pro obnovení (beta) Přeskočit obnovení souborů Chápu Obnovují se soubory… @@ -204,4 +204,22 @@ Opětovná instalace Znovu nainstalováno Umístění zvolené uživatelem + Zálohy zařízení na zařízení + Tato možnost vynutí zálohy u většiny aplikací, i když je dané aplikace zakazují. Jedná se o možnost ve fázi alfa, používejte ji na vlastní nebezpečí. + Oznámení o úspěchu + Další záloha: %1$s + Další záloha (přibližně): %1$s + po splnění podmínek + Zálohy budou provedeny automaticky po připojení USB disku + Plánování záloh + Týdně + Podmínky + Frekvence záloh + Každých 12 hodin + Denně + Každé 3 dny + Zálohovat při používání mobilních dat + Zálohovat pouze při nabíjení + Zálohování souboru APK aplikace %s + Ukládání seznamu aplikací, které nelze zálohovat. \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6702f253d..eaab11c45 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -204,4 +204,7 @@ Eine Liste der Mitwirkenden findest du auf GitHub. Mitwirkende Organisationen Benutzerdefinierter Speicherort + Erfolgsbenachrichtigung + Gerät-zu-Gerät-Sicherungen + Dies erzwingt Sicherungen für die meisten Apps, auch wenn diese dies nicht zulassen. Dies ist experimentell, die Verwendung erfolgt auf eigene Gefahr. \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index df89abbe7..eeac5ad31 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -145,8 +145,8 @@ Αντίγραφο ασφαλείας εφαρμογών Δημιουργία αντιγράφου ασφαλείας των αρχείων μου Κανένα - Πειραματικό χαρακτηριστικό - Η δημιουργία αντιγράφου ασφαλείας αρχείων είναι ακόμη πειραματική και μπορεί να μην λειτουργήσει. Μην βασίζεστε σε αυτή για σημαντικά δεδομένα. + Χαρακτηριστικό beta + Η δημιουργία αντιγράφου ασφαλείας αρχείων είναι σε στάδιο beta και μπορεί να μην λειτουργήσει. Μην βασίζεστε σε αυτή για σημαντικά δεδομένα. Τα υπάρχοντα αντίγραφα ασφαλείας σε αυτήν την τοποθεσία θα διαγραφούν. Νέος κωδικός Προηγμένες ρυθμίσεις @@ -156,12 +156,12 @@ Εισάγετε εκ νέου το κλείδωμα οθόνης Παράλειψη επαναφοράς εφαρμογών Παράλειψη επαναφοράς αρχείων - Επιλέξτε ένα αντίγραφο ασφαλείας χώρου αποθήκευσης για επαναφορά (πειραματικό) + Επιλέξτε ένα αντίγραφο ασφαλείας χώρου αποθήκευσης για επαναφορά (beta) Το κατάλαβα Περιλαμβανόμενα αρχεία και φάκελοι Ενεργοποίηση ούτως ή άλλως Προειδοποίηση: Τα αυτόματα αντίγραφα ασφαλείας απενεργοποιήθηκαν επειδή η βελτιστοποίηση μπαταρίας είναι ενεργή. - Δημιουργία αντιγράφου ασφαλείας χώρου αποθήκευσης (πειραματικό) + Δημιουργία αντιγράφου ασφαλείας χώρου αποθήκευσης (beta) Απαιτείται νέος κωδικός επαναφοράς Για να συνεχίσετε να χρησιμοποιείτε τα αντίγραφα ασφαλείας εφαρμογών, πρέπει να δημιουργήσετε έναν νέο κωδικό επαναφοράς. \n @@ -204,4 +204,22 @@ Ινστιτούτο Calyx για χρήση στο CalyxOS \nNGI0 PET χρηματοδότηση από την NLnet Τοποθεσία επιλεγμένη από το χρήστη + Αντίγραφα ασφαλείας από συσκευή σε συσκευή + Αυτό αναγκάζει τη δημιουργία αντιγράφων ασφαλείας για τις περισσότερες εφαρμογές, ακόμη και όταν το απαγορεύουν. Αυτό είναι σε στάδιο alpha, χρησιμοποιήστε με δική σας ευθύνη. + μόλις πληρούνται οι προϋποθέσεις + Προγραμματισμός δημιουργίας αντιγράφων ασφαλείας + Συχνότητα αντίγραφων ασφάλειας + Κάθε 12 ώρες + Καθημερινά + Κάθε 3 ημέρες + Εβδομαδιαία + Προϋποθέσεις + Δημιουργία αντιγράφων ασφαλείας κατά τη χρήση δεδομένων κινητής τηλεφωνίας + Δημιουργία αντιγράφων ασφαλείας μόνο κατά τη φόρτιση + Δημιουργία αντιγράφου ασφαλείας APK του %s + Επόμενο αντίγραφο ασφαλείας (εκτίμηση): %1$s + Ειδοποίηση επιτυχίας + Επόμενο αντίγραφο ασφαλείας: %1$s + Τα αντίγραφα ασφαλείας θα συμβούν αυτόματα όταν συνδέσετε τη μονάδα USB + Αποθήκευση λίστας εφαρμογών για τις οποίες δεν μπορούμε να δημιουργήσουμε αντίγραφα ασφαλείας. \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 09f341e08..5c6e37140 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -159,12 +159,12 @@ Listo Archivos y carpetas incluidos Copia de seguridad de la app - Copia de seguridad del almacenamiento (experimental) - Característica experimental - La copia de seguridad de los archivos es todavía experimental y podría no funcionar. No confíe en ella para los datos importantes. + Copia de seguridad del almacenamiento (fase beta) + Característica en fase beta + La copia de seguridad de los archivos es todavía en fase beta y podría no funcionar. No confíe en ella para los datos importantes. No instalada No restaurar aplicaciones - Elija una copia de seguridad de almacenamiento para restaurar (experimental) + Elija una copia de seguridad de almacenamiento para restaurar (fase beta) Sus archivos se están restaurando en segundo plano. Puede empezar a usar su teléfono mientras se está ejecutando. \n \nAlgunas aplicaciones (por ejemplo, Signal o WhatsApp) pueden requerir que los archivos estén completamente restaurados para importar una copia de seguridad. Intente evitar iniciar esas aplicaciones antes de que la restauración de los archivos se haya completado. @@ -204,4 +204,22 @@ Colaboradores En GitHub encontrará una lista de colaboradores. Ubicación elegida por el usuario + Copias de seguridad de dispositivo a dispositivo + Esto obliga a realizar copias de seguridad para la mayoría de las aplicaciones, incluso cuando no las permiten. Esta en fase alpha, úselo bajo su propio riesgo. + Notificación exitosa + Las copias de seguridad se realizarán automáticamente después de conectar la unidad USB + Cada 3 días + Semanalmente + Condiciones + Copia de seguridad cuando utilice datos móviles + Copia de seguridad del APK de %s + Guardando la lista de aplicaciones de las que no podemos hacer una copia de seguridad. + Siguiente copia de seguridad: %1$s + Próxima copia de seguridad (estimación): %1$s + una vez cumplidas las condiciones + Programación de copias de seguridad + Frecuencia de la copia de seguridad + Cada 12 horas + Diariamente + Copia de seguridad solo cuando se carga \ No newline at end of file diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 173877e14..89b4937b6 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -1,6 +1,6 @@ - %1$s vapaana + %1$s vapaana Versio: %s Varmuuskopiosovellus, joka käyttää Androidin sisäistä varmuuskopiosovellusrajapintaa. Tietoa diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 17f12e0e9..27e55cab3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -65,7 +65,7 @@ Nota: è necessario collegare %1$s per permettere il ripristino. Stato backup e impostazioni Backup Seedvault - %1$s liberi + %1$s liberi Versione: %s Una applicazione di backup facente uso della API interna di backup di Android. Ulteriori info diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 8b939a839..92c4d83ef 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -203,4 +203,8 @@ מכון Calyx לשימוש ב־CalyxOS \nקרן NGI0 PET מבית NLnet מקום לבחירת המשתמש + גיבויי מכשיר למכשיר + כופה את גיבוי רוב היישומונים, אפילו אם הם מסרבים לזה. יכולת ניסיונית ראשית, נא להשתמש במשנה זהירות. + התראה על הצלחה + %1$d מתוך %2$d יישומונים גובו (%3$s). למידע נוסף נא לגעת. \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 7c7387444..62a573551 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -55,7 +55,7 @@ バックアップを保存する場所を選択してください 既存のコードを確認するか、新しいコードを生成します アカウントが見つかりません。新しくセットアップします。 (またはパスコードを無効にします)。 - アプリ自体をバックアップします。そうしないと、アプリデータのみがバックアップされます。 + アプリ自体をバックアップしてください。さもなければ、アプリのデータのみがバックアップされます。 このアプリについて 12 単語のリカバリコードを入力して、必要なときに機能することを確認してください。 %1$s の空き diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 17fc23add..c49e1a688 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -143,12 +143,12 @@ Powiadomienie o kopii zapasowej Pomyślnie utworzono nowy kod przywracania Kopia zapasowa aplikacji - Kopia zapasowa plików jest w fazie eksperymentalnej i może nie działać. Nie polegaj na niej dla ważnych danych. + Tworzenie kopii zapasowych plików jest w wersji beta i może nie działać. Nie należy na niej polegać w przypadku ważnych danych. Dodane pliki i foldery - Kopia zapasowa pamięci masowej (eksperymentalne) + Kopia zapasowa pamięci masowej (beta) Kopia zapasowa plików Żadne - Funkcjonalność eksperymentalna + Funkcja w wersji beta Włącz mimo wszystko Uwaga: Brak automatycznych kopii zapasowych, ponieważ optymalizacja baterii jest włączona. Wymagany nowy kod odzyskiwania @@ -185,7 +185,7 @@ Błąd: Nie można zapisać dziennika aplikacji Lokalizacja wybrana przez użytkownika Zrozumiano - Wybierz kopię zapasową do przywrócenia (eksperymentalne) + Wybierz kopię zapasową do przywrócenia (beta) Twoje pliki są przywracanę w tle. Możesz używać telefonu w trakcie przywracania. \n \nNiektóre aplikacje (np. Signal czy WhatsApp) mogę wymagać by pliki były w pełni przywrócona aby zaimportować kopię zapasową. Postaraj się nie włączć tych aplikacji przed zakończeniem przywracania. @@ -200,4 +200,26 @@ \n \nUwaga: Plik z raportem błędów może zawierać informacje na podstawie których można cię zidentyfikować. Zapoznaj się z nim i usuń go gdy nie będzie ci już potrzebny! Wprowadź informacje swojego urządzenia aby kontynuować + Kopie zapasowe będą tworzone automatycznie po podłączeniu dysku USB + po spełnieniu warunków + Harmonogram tworzenia kopii zapasowych + Wejdź na stronę GitHuba lista współautorów. + Calyx Institutedo użytku wCalyxOS +\nNGI0 PET sfinansowany przez NLnet + Organizacje współpracujące + Kolejna kopia zapasowa: %1$s + Kolejna kopia zapasowa (szacunkowo):%1$s + Częstotliwość tworzenia kopii zapasowych + Co 12 godzin + Codziennie + Co 3 dni + Tygodniowo + Warunki + Tworzenie kopii zapasowych podczas korzystania z danych mobilnych + Tworzenie kopii zapasowej tylko podczas ładowania + Powiadomienie o powodzeniu + Tworzenie kopii zapasowej APK %s + Zapisywanie listy aplikacji, których kopii zapasowej nie możemy utworzyć. + Kopie zapasowe między urządzeniami + Wymusza to tworzenie kopii zapasowych dla większości aplikacji, nawet jeśli na to nie zezwalają. Funkcja w wersji alfa, używasz na własne ryzyko. \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index e49ed648d..70ef4336a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1,17 +1,17 @@ - Conecte seu %1$s antes de instalar o aplicativo para restaurar seus dados do backup. + Conecte seu %1$s antes de instalar o aplicativo para restaurar seus dados de backup. Não foi possível restaurar os dados de %1$s - Erro de restauração automática do pen drive + Erro de restauração automática do pendrive Consertar - Falha ao executar um backup do dispositivo. - Erro de backup + Falha na execução de um backup do dispositivo. + Erro do backup Notificação de erro - Backup falhou + O backup falhou Backup concluído Backup em execução - Notificação de backup - Seu código é inválido. Por favor, verifique todas as palavras e tente novamente! + Notificação do backup + Seu código está inválido. Verifique todas as palavras e suas posições e tente novamente! Palavra errada. Você se esqueceu de inserir esta palavra. Palavra 12 @@ -26,163 +26,200 @@ Palavra 3 Palavra 2 Palavra 1 - Verifique - Digite o código de recuperação de 12 palavras que você anotou ao configurar backups. - Digite seu código de recuperação de 12 palavras para garantir que funcionará quando você precisar. + Verificar + Digite o código de recuperação de 12 palavras que você anotou quando configurou os backups. + Digite seu código de recuperação de 12 palavras para garantir que ele funcionará quando você precisar. Confirmar código - Escreva no papel agora! - Você precisa do seu código de recuperação de 12 palavras para restaurar os dados de backup. + Agora, escreva o código no papel! + Você precisa do seu código de recuperação de 12 palavras para restaurar os dados do backup. Código de recuperação Voltar - Não foi possível obter permissão para gravar no local do backup. + Não foi possível obter permissão para escrita no local do backup. Ocorreu um erro ao acessar o local do backup. Procurando por backups… - Inicializando local de backup… - Clique para configurar uma conta - Clique para instalar - %1$s livre - Precisa ser conectado - Pen drive USB - Deseja realmente desativar o backup de aplicativo\? - Pessoas com acesso ao seu local de armazenamento podem saber quais aplicativos você usa, mas não têm acesso aos dados dos aplicativos. - Onde encontrar seus backups\? - Escolha onde armazenar backups + Inicializando o local do backup… + Toque para configurar uma conta + Toque para instalar + %1$s livre(s) + Precisa estar conectado + Pendrive USB + Deseja realmente desativar o backup dos próprios aplicativos? + Pessoas com acesso ao seu local de armazenamento podem saber quais aplicativos você usa, mas não tem acesso aos dados dos aplicativos. + Onde encontramos o seu backup? + Escolha onde armazenar os backups Fazer backup agora Excluir aplicativos Último backup: %1$s - Desativar backup de aplicativo + Desativar o backup de aplicativos Cancelar - O backup de aplicativo desativado ainda fará o backup dos dados do aplicativo. Porém, ele não será restaurado automaticamente. + O backup de aplicativos desativado ainda realizará o backup dos dados dos aplicativos. Porém, eles não serão restaurados automaticamente. \n -\nVocê precisará instalar todos os seus aplicativos manualmente enquanto o \"Restauração automática\" está ativado. - Faça backup dos aplicativos neles mesmos. Caso contrário, apenas os dados do aplicativo seriam armazenados em backup. - Backup de dados de aplicativo - Backup de aplicativo +\nVocê precisará instalar todos os seus aplicativos manualmente com a opção \"Restauração automática\" ativada. + Fazer backup dos próprios aplicativos. Quando desativado, apenas os dados do aplicativo serão armazenados no backup. + Backup dos dados do aplicativo + Backup de aplicativos Observação: seu %1$s precisa estar conectado para que isso funcione. - Ao reinstalar um aplicativo, restaura as configurações e dados do backup. + Ao reinstalar um aplicativo, restaurar as configurações e outros dados de seu backup. Restauração automática - Você escolheu o armazenamento interno para seu backup. Isto não estará disponível quando seu telefone for perdido ou quebrado. - Todos os backups são criptografados em seu telefone. Para restaurar do backup, você precisará do código de recuperação de 12 palavras. + Você escolheu o armazenamento interno para seu backup. Ele não estará disponível caso seu dispositivo for perdido ou quebrado. + Todos os backups são criptografados em seu dispositivo. Para restaurar o backup, você precisará do seu código de recuperação de 12 palavras. Nunca Armazenamento interno Nenhum - Local de backup - Faça backup de meus aplicativos + Local do backup + Fazer backup dos meus aplicativos Restaurar backup Backup Ocorreu um erro ao carregar os backups. - Não podemos encontrar nenhum backup neste local. + Não conseguimos encontrar nenhum backup neste local. \n \nEscolha outro local que contenha uma pasta %s. - Sem backups encontrados + Nenhum backup encontrado Não restaurar Último backup %1$s · Primeiro %2$s. Escolha um backup para restaurar Restaurar do backup Desinstalar aplicativo - Um aplicativo de backup usando a API de backup interna do Android. + Um aplicativo de backup que usa a API de backup interna do Android. É software gratuito, lançado sob a licença \"Apache 2\". +\n +\nComo todo software, o Seedvault pode conter bugs ou vulnerabilidades. Sobre - Usar assim mesmo - Escolha outro - Atenção + Usar mesmo assim + Escolher outro + Aviso Terminar Ocorreu um erro ao restaurar o backup. - Restauração completa + Restauração completada A cota de backup foi excedida - Aplicativo não instalado + Aplicativo não foi instalado O aplicativo não permitiu o backup - O aplicativo informou de que não há dados para backup - Gerenciador de pacote do sistema + O aplicativo informou que não há dados para fazer backup + Gerenciador de pacotes do sistema Restaurando backup Próximo Reinstalando aplicativos - Nenhum backup adequado encontrado no local dado. + Nenhum backup utilizável encontrado no local informado. \n \nIsso provavelmente se deve a um código de recuperação errado ou a um erro de armazenamento. - %1$d de %2$d aplicativos com backup (%3$s). Toque para saber mais. - Backup já em andamento - Não feito nenhum backup desde que não foi utilizado recentemente - Nenhum backup foi feito ainda + Backup %1$d de %2$d aplicativos (%3$s). Toque para saber mais. + Backup já está em andamento + O backup não foi feito pois não tinha sido utilizado recentemente + O backup não foi feito ainda Conta não disponível. Configure uma (ou desative a senha). - Status e configurações do Backup - Backup Seedvault - Incapaz de restaurar alguns apps - Alguns apps não foram instalados + Status e configurações do backup + Backup do Seedvault + Não foi possível restaurar alguns aplicativos + Alguns aplicativos não foram instalados Toque para instalar - Dados só podem ser restaurados se um app estiver instalado. + Dados só podem ser restaurados se o aplicativo estiver instalado. \n -\nToque em apps falhos para tentar instalá-los manualmente antes de proceder. - Apps que não permitem backup de dados - Apps instalados +\nToque em aplicativos com falha na instalação para tentar instalá-los manualmente antes de continuar. + Aplicativos que não permitem backup dos dados + Aplicativos instalados Mensagens de texto SMS - Apps do sistema + Aplicativos do sistema Status do backup Versão: %s Você pode reinstalar esses aplicativos manualmente e a restauração automática tentará restaurar seus dados (quando ativada). - A cota de backup foi excedida - Nenhum backup foi feito desde que não foi usado recentemente - Aguardando pelo backup… + Cota de backup excedida + O backup não foi feito pois não foi usado recentemente + Aguardando para fazer backup… Contatos locais Histórico de chamadas Configurações do dispositivo - Novo código de recuperação foi criado com sucesso - Gerar um novo código fará seus backups existentes inacessíveis. Tentaremos deletá-los se possível. + Um novo código de recuperação foi criado com sucesso + Gerar um novo código fará com que seus backups existentes fiquem inacessíveis. Tentaremos excluí-los se possível. \n -\nTem certeza que quer fazer isso\? - Espere um segundo… +\nTem certeza que quer fazer isso? + Só um momento… Gerar um novo código Tente novamente - Você inseriu um código de recuperação inválido. Por favor, tente novamente. + Você inseriu um código de recuperação inválido. Tente novamente! \n \nSe você perdeu seu código, toque em \'Gerar um novo código\' abaixo. Código de recuperação incorreto - Seu código está correto e funcionará para restaurar o seu backup. + Seu código está correto e funcionará quando restaurar o seu backup. Código de recuperação verificado Verifique o código existente ou gere um novo Código de recuperação Nenhum - Recurso experimental - Habilitar mesmo assim + Recurso beta + Ativar mesmo assim Configurações para especialistas - Cota ilimitada de aplicativos - Não imponha uma limitação ao tamanho dos backups de aplicativos. + Cota ilimitada para aplicativos + Não impõe uma limitação no tamanho do backup dos aplicativos. \n \nAviso: isso pode encher seu local de armazenamento rapidamente. Não é necessário para a maioria dos aplicativos. Não instalado - Pular restauração de aplicativos + Pular a restauração dos aplicativos Os backups existentes neste local serão excluídos. - Arquivos e pastas incluídos - Isto pode levar algum tempo… + Pastas e arquivos incluídos + Isso pode levar algum tempo… Digite as credenciais do seu dispositivo para continuar - O backup de arquivos ainda é experimental e pode não funcionar. Não confie nele para dados importantes. - Backup de armazenamento (experimental) - Faça backup dos meus arquivos + O backup de arquivos está em beta e pode não funcionar. Não dependa dele para salvar dados importantes. + Backup do armazenamento (beta) + Fazer backup dos meus arquivos Entendi - Digite novamente o bloqueio de tela + Digite novamente o seu bloqueio de tela Pular restauração de arquivos - Os arquivos estão sendo restaurados … - Escolha um backup de armazenamento para restaurar (experimental) - Backup de aplicativo - Seus arquivos estão sendo restaurados em segundo plano. Você pode usar seu telefone durante a execução. + Os arquivos estão sendo restaurados… + Escolha um backup de armazenamento para restaurar (beta) + Backup de aplicativos + Seus arquivos estão sendo restaurados em segundo plano. Você pode usar seu dispositivo durante a execução. \n -\nAlguns aplicativos (por exemplo, Signal ou WhatsApp) podem exigir que os arquivos sejam totalmente restaurados para importar um backup. Tente evitar iniciar esses aplicativos antes que a restauração do arquivo seja concluída. - Aviso: sem backups automáticos, pois a otimização de bateria está ativa. +\nAlguns aplicativos (por exemplo, Signal ou WhatsApp) podem exigir que os arquivos sejam totalmente restaurados para importar um backup. Tente evitar iniciar esses aplicativos antes que a restauração do arquivo esteja concluída. + Aviso: Backups automáticos não estão ativos porque a otimização de bateria está ativa. Novo código de recuperação necessário Novo código - Para continuar usando os backups de aplicativos, você precisa gerar um novo código de recuperação. + Para continuar usando o backup de aplicativos, você precisa gerar um novo código de recuperação. \n \nLamentamos o inconveniente. Backups desativados - Gere um novo código de recuperação para concluir a atualização e continuar a usar os backups. + Gere um novo código de recuperação para concluir a atualização e continuar usando os backups. Erro: Nenhuma loja de aplicativos instalada Dados restaurados Reinstalado Não foi possível instalar - Aviso de restauração + Aviso da restauração Fazendo backup - Aviso de backup + Aviso do backup Reinstalando Falha na restauração - Backup desabilitado - Desligar mesmo assim + Backup desativado + Desativar mesmo assim + Montagem WebDAV não disponível. Configure uma. + Contribuidores + Organizações Contribuidoras + Salvar log do aplicativo + %1$s (Não é recomendado) + Os desenvolvedores podem diagnosticar bugs com esses logs. +\n +\nAviso: O arquivo de log pode conter informações de identificação pessoal. Analise antes e o exclua após compartilhar! + Quando você ativar os backups novamente, o processo de backup pode demorar mais do que normal e utilizará mais espaço de armazenamento. + Deseja realmente desativar o backup de aplicativos? + Erro: Não foi possível salvar o log do aplicativo + Local escolhido pelo usuário + Toque para configurar uma montagem WebDAV + Backup concluído + Instituto Calyx para o uso no CalyxOS +\nNGI0 PET Fund por NLnet + Backups de dispositivo para dispositivo + Isso força o backup para a maioria dos aplicativos, mesmo quando eles não permitem. Isso está em alpha, use por conta própria. + Veja a lista de contribuidores no GitHub. + Notificação de sucesso + Próximo backup: %1$s + Próximo backup (estimado): %1$s + Agendamento do backup + quando as condições forem cumpridas + Frequência do backup + A cada 12 horas + Diariamente + A cada 3 dias + Semanalmente + Condições + Fazer backup somente quando o dispositivo estiver carregando + Fazendo backup do APK de %s + Fazer backup mesmo ao usar dados móveis + Salvando uma lista dos aplicativos que não podemos fazer backup. + Backups serão feitos automaticamente quando você conectar seu dispositivo de armazenamento USB \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index e05e5f189..eaaeb5e0c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -2,7 +2,9 @@ Feito o backup da app %1$d de %2$d (%3$s). Toque para saber mais. Backup em andamento - Uma app de backup usando a API de backup interna do Android. + Um aplicativo de backup que usa a API de backup interna do Android. É software gratuito, lançado sob a licença \"Apache 2\". +\n +\nComo todo software, o Seedvault pode conter bugs ou vulnerabilidades. Sobre Usar assim mesmo Escolha outro @@ -143,12 +145,12 @@ Não instalado Entendi Backup de app - Backup de armazenamento (experimental) + Backup do armazenamento (beta) Faça backup dos meus ficheiros Ficheiros e pastas incluídos Nenhum - Recurso experimental - O backup de ficheiros ainda é experimental e pode não funcionar. Não confie nele para dados importantes. + Recurso beta + O backup de arquivos está em beta e pode não funcionar. Não dependa dele para salvar dados importantes. Ativar mesmo assim Configurações para especialistas Cota ilimitada de apps @@ -161,7 +163,7 @@ Digite as credenciais do seu aparelho para continuar Pular restauração de apps Pular restauração de ficheiros - Escolha um backup de armazenamento para restaurar (experimental) + Escolha um backup de armazenamento para restaurar (beta) Os ficheiros estão sendo restaurados … Os seus ficheiros estão sendo restaurados em segundo plano. Pode usar o seu telefone durante a execução. \n @@ -187,4 +189,37 @@ Desligar mesmo assim Salvar registro de app Erro: Não foi possível salvar os registros do app + Local escolhido pelo usuário + Fazer backup mesmo ao usar dados móveis + Instituto Calyx para o uso no CalyxOS +\nNGI0 PET Fund por NLnet + Toque para configurar uma montagem WebDAV + Contribuidores + Veja uma lista de contribuidores no GitHub. + Organizações Contribuidoras + Quando você ativar os backups novamente, o processo de backup pode demorar mais do que normal e utilizará mais espaço de armazenamento. + Deseja realmente desativar o backup de aplicativos? + Próximo backup: %1$s + Próximo backup (estimado): %1$s + quando as condições forem cumpridas + Backups serão feitos automaticamente quando você conectar seu dispositivo de armazenamento USB + Agendamento do backup + Frequência do backup + A cada 12 horas + Diariamente + A cada 3 dias + Semanalmente + Condições + Fazer backup somente quando o dispositivo estiver carregando + Os desenvolvedores podem diagnosticar bugs com esses logs. +\n +\nAviso: O arquivo de log pode conter informações de identificação pessoal. Analise antes e o exclua após compartilhar! + Isso força o backup para a maioria dos aplicativos, mesmo quando eles não permitem. Isso está em alpha, use por conta própria. + %1$s (Não é recomendado) + Montagem WebDAV não disponível. Configure uma. + Notificação de sucesso + Fazendo backup do APK de %s + Salvando uma lista dos aplicativos que não podemos fazer backup. + Backup concluído + Backups de dispositivo para dispositivo \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index a6b3daec9..c350b095c 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1,2 +1,187 @@ - \ No newline at end of file + + Copii de rezervă aplicații + Dezactivezi cu adevărat crearea copiilor de rezervă pentru aplicații? + Dezactivarea copiilor de rezervă pentru aplicații nu va dezactiva crearea copiilor de rezervă pentru datele aplicațiilor. Cu toate acestea, nu se va restaurat automat. +\n +\nVa trebui să instalezi manual toate aplicațiile în timp ce „Restaurare automată” este activată. + Anulează + Dezactivează crearea copiilor de rezervă pentru aplicații + Stare copii de rezervă + Copii de rezervă + Se caută copii de rezervă… + Avertisment copie de rezervă + Nu s-a putut instala + Înainte + Alege o copie de rezervă de stocare pentru restaurare (beta) + Am înțeles + Avertisment + Starea și setările copiei de rezervă + Restaurează copia de rezervă + Stocare internă + Niciodată + Copii de rezervă Seedvault + Copii de rezervă pentru aplicațiile mele + Copii de rezervă pentru aplicațiile în sine. În caz contrar, se vor crea copii de rezervă numai pentru datele aplicațiilor. + Când activezi din nou copiile de rezervă , procesul de creare a copiilor de rezervă poate dura mai mult decât de obicei și va folosi spațiu de stocare suplimentar. + Toate copiile de rezervă sunt criptate pe telefon. Pentru a restabili din copia de rezervă, vei avea nevoie de codul de recuperare din 12 cuvinte. + La reinstalarea unei aplicații, se restaurează setările și datele pentru care s-au făcut copie de rezervă . + Crearea copiilor de rezervă pentru fișiere este regim beta și este posibil să nu funcționeze. Nu te baza pe ea pentru datele importante. + Avertisment: Fără crearea automată a copiilor de rezervă, optimizarea bateriei este activă. + Locația copiei de rezervă + Niciuna + Acest lucru forțează crearea copiilor de rezervă pentru majoritatea aplicațiilor, chiar și atunci când acestea le interzic. Aceasta este regim alfa, utilizează pe propriul risc. + Dezvoltatorii pot diagnostica erori cu aceste jurnale. +\n +\nAvertisment: fișierul jurnal poate conține informații de identificare personală. Examinează înainte și șterge după distribuire! + %1$s (Nu se recomandă) + Atinge pentru a instala + Atinge pentru a configura o montare WebDAV + Se inițializează locația copiilor de rezervă… + Acest lucru poate dura ceva timp… + Nu se poate obține permisiunea de a scrie în locația copiilor de rezervă. + Cod de recuperare + Copiere de rezervă a datelor aplicațiilor + Organizații care contribuie + Restaurare din copia de rezervă + Alege o copie de rezervă pentru restaurare + Ultima copie de rezervă %1$s · Prima %2$s. + Omite restabilirea aplicațiilor + Nu s-au găsit copii de rezervă + Nu s-au găsit copii de rezervă în această locație. +\n +\nAlege o altă locație care conține un dosar %s. + A apărut o eroare la încărcarea copiilor de rezervă. + Ai ales stocarea internă pentru crearea copiilor de rezervă. Aceasta nu va fi disponibilă atunci când telefonul este pierdut sau spart. + Cod de recuperare + Suportul WebDAV nu este disponibil. Configurează unul. + Înapoi + Notează-le pe hârtie acum! + Pentru %1$d din %2$d aplicații s-a creat copie de rezervă (%3$s). Atinge pentru a afla mai multe. + Crearea copiei de rezervă a eșuat + Notificare de eroare + Eroare la crearea copiei de rezervă + Manager pachete de sistem + Restaurare completă + A apărut o eroare la restaurarea copiei de rezervă. + Finalizează + Fișierele sunt restaurate… + Fișierele sunt restaurate în fundal. Poți începe să utilizezi telefonul în timp ce aceasta rulează. +\n +\nUnele aplicații (de exemplu, Signal sau WhatsApp) ar putea necesita restaurarea completă a fișierelor pentru a importa o copie de rezervă. Încearcă să eviți pornirea acelor aplicații înainte ca restaurarea fișierelor să fie finalizată. + O aplicație pentru crearea copiilor de rezervă care utilizează API-ul intern al Android. Este Software Liber, lansat sub licență Apache 2. +\n +\nCa toate programele, Seedvault poate conține erori sau vulnerabilități. + Copii de rezervă pentru aplicații + Copie de rezervă pentru stocare (beta) + Creează copie de rezervă fișierelor mele + Fișiere și dosare incluse + A apărut o eroare la accesarea locației copiilor de rezervă. + Creare copie de rezervă finalizată + Cota copiilor de rezervă a fost depășită + Nu restaura + Reinstalat + Pentru a continua să utilizezi copiile de rezervă ale aplicației, trebuie să generezi un nou cod de recuperare. +\n +\nNe cerem scuze pentru neplăcere. + Este necesar un nou cod de recuperare + Cod nou + Cuvântul 6 + Cuvântul 7 + Cod de recuperare incorect + Generează un nou cod de recuperare pentru a finaliza actualizarea și a continua să utilizezi copii de rezervă. + Copii de rezervă dezactivate + Aplicația nu a permis crearea copiei de rezervă + Restaurând copia de rezervă + Omite restaurarea fișierelor + Reîncearcă + Contribuitori + Calyx Institute pentru utilizare în CalyxOS +\nFondul NGI0 PET de NLnet + Crearea copiilor de rezervă nu este activată + Remediază + Eroare de restaurare automată a unității flash + Nu s-au putut restaura datele pentru %1$s + Conectează %1$s înainte de a instala aplicația pentru a-i restaura datele din copia de rezervă. + Dezinstalează aplicația + Folosește oricum + Salvează jurnalul aplicației + Flash Drive USB + Verifică + Copiile de rezervă existente în această locație vor fi șterse. + Trebuie conectat + Cuvântul 1 + Cuvântul 11 + Cuvântul 5 + Cuvântul 12 + Cuvântul 8 + Cuvântul 9 + Cuvânt greșit. + Cuvântul 10 + Noul cod de recuperare a fost creat cu succes + Reintrodu blocarea ecranului + Introdu acreditările dispozitivului pentru a continua + Despre + Nu s-au găsit copii de rezervă adecvate în această locație. +\n +\nAcest lucru se datorează cel mai probabil unui cod de recuperare greșit sau unei erori de stocare. + Reinstalarea aplicațiilor + Reinstalare + Date restaurate + Restaurarea a eșuat + Avertisment de restabilire + Eroare: nu este instalat niciun magazin de aplicații + Alege alta + Consultă GitHub pentru o listă a colaboratorilor. + Oprește oricum + Restaurare automată + Notă: %1$s trebuie conectat la rețea pentru ca acest lucru să funcționeze. + Dezactivezi cu adevărat crearea copiilor de rezervă pentru aplicații? + Ultima copie de rezervă: %1$s + Excludere aplicații + Creează o copie de rezervă acum + Nimic + Verifică codul existent sau generează unul nou + Caracteristică beta + Activează oricum + Setări expert + Cotă nelimitată pentru aplicații + Nu impune o limitare a dimensiunii copiilor de rezervă ale aplicațiilor. +\n +\nAvertisment: acest lucru poate umple rapid locația de stocare. Nu este necesar pentru majoritatea aplicațiilor. + Copii de rezervă de la dispozitiv la dispozitiv + Eroare: nu s-a putut salva jurnalul aplicației + Alege unde să fie stocate copiile de rezervă + Unde să găsesc copiile de rezervă? + Persoanele cu acces la locația de stocare pot afla ce aplicații folosești, dar nu au acces la datele aplicațiilor. + Locație aleasă de utilizator + %1$s liber + Atinge pentru a configura contul + Nu este instalat + Ai nevoie de codul de recuperare din 12 cuvinte pentru a restabili datele din copia de rezervă. + Confirmă codul + Introdu codul de recuperare din 12 cuvinte pentru a te asigura că va funcționa atunci când ai nevoie de el. + Introdu codul de recuperare din 12 cuvinte pe care l-ai notat când ai configurat copiile de rezervă. + Creare reușită a copiei de rezervă + Notificare de succes + Rulează crearea copiei de rezervă + Cuvântul 2 + Cuvântul 3 + Cuvântul 4 + Ai uitat să introduci acest cuvânt. + Codul este greșit. Verifică toate cuvintele, precum și poziția lor și încearcă din nou! + Cod de recuperare verificat + Codul este corect și va funcționa pentru restaurarea copiei de rezervă. + Ai introdus un cod de recuperare greșit. Încearcă din nou! +\n +\nDacă ai pierdut codul, apasă pe „Generează cod nou” de mai jos. + Generează un cod nou + Așteaptă o secundă… + Generarea unui cod nou va face copiile de rezervă existente inaccesibile. Vom încerca să le ștergem dacă este posibil. +\n +\nEști sigur că vrei să faci asta? + Notificare copie de rezervă + O copie de rezervă a dispozitivului nu a putut fi executată. + Creare copii de rezervă + Aplicația nu este instalată + \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 51767491f..26116d5d1 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -11,4 +11,8 @@ Ska backup för appar verkligen stängas av\? När du återaktiverar säkerhetskopior kommer processen att ta längre tid än normalt samt nyttja ytterligare lagringsutrymme. Stäng av ändå + Avbryt + Senaste säkerhetskopiering: %1$s + Nästa säkerhetskopiering: %1$s + Näst säkerhetskopiering (uppskattning): %1$s \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1cc74b407..d0953daf2 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -147,7 +147,7 @@ \n \nBazı uygulamalar (örn. Signal veya WhatsApp), bir yedeklemeyi içe aktarmak için dosyaların tamamen geri yüklenmesini gerektirebilir. Dosya geri yükleme işlemi tamamlanmadan bu uygulamaları başlatmaktan kaçının. Dosyalar geri yükleniyor… - Geri yüklenecek bir depolama yedeklemesi seçin (deneysel) + Geri yüklenecek bir depolama yedeklemesi seçin (beta aşamasında) Dosyaları geri yüklemeyi atla Uygulamaları geri yüklemeyi atla Devam etmek için aygıt kimlik bilgilerinizi girin @@ -161,12 +161,12 @@ Sınırsız uygulama kotası Uzman ayarları Yine de etkinleştir - Dosyaları yedeklemek hala deneyseldir ve çalışmayabilir. Önemli veriler için ona güvenmeyin. - Deneysel özellik + Dosyaları yedeklemek beta aşamasındadır ve çalışmayabilir. Önemli veriler için ona güvenmeyin. + Beta aşamasındaki özellik Yok Dahil edilen dosya ve klasörler Dosyalarımı yedekle - Depolama yedekle (deneysel) + Depolama yedekle (beta aşamasında) Uygulama yedekle Uyarı: Pil iyileştirme etkin olduğundan otomatik yedekleme yok. Yeni kurtarma kodu gerekli @@ -204,4 +204,22 @@ Katkıda bulunanların listesi için lütfen GitHub\'a bakın. Katkıda Bulunan Kuruluşlar Kullanıcının seçtiği konum + Aygıttan aygıta yedeklemeler + Bu, izin vermeseler bile çoğu uygulama için yedeklemeyi zorlar. Bu alfa aşamasındadır, riski size ait olmak üzere kullanın. + Başarı bildirimi + USB belleğinizi taktığınızda yedeklemeler otomatik olarak gerçekleşecek + Yedekleme zamanlaması + 12 saatte bir + 3 günde bir + Koşullar + Mobil veri kullanırken yedekle + Yalnızca şarj olurken yedekle + Yedeklenemeyen uygulamaların listesi kaydediliyor. + Sonraki yedekleme: %1$s + Sonraki yedekleme (tahmini): %1$s + koşullar sağlandığında + Yedekleme sıklığı + Günlük + Haftalık + %s APK\'sı yedekleniyor \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 86a68a71d..d659787f6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -147,7 +147,7 @@ \n \n某些应用程序(例如 Signal 或 WhatsApp)可能在文件完全恢复后才能导入备份。尽量避免在文件恢复完成之前启动这些应用程序。 正在恢复文件… - 选择要恢复的存储备份(实验性) + 选择要恢复的存储备份(测试) 跳过恢复文件 跳过恢复应用程序 输入您的设备凭据以继续 @@ -161,12 +161,12 @@ 无限应用配额 专家设置 仍要启用 - 文件备份仍处于试验阶段,可能无法正常工作。不要依靠它备份重要数据。 - 实验性功能 + 文件备份仍处于测试阶段,可能无法正常工作。不要依靠它备份重要数据。 + 测试功能 包括的文件和文件夹 备份我的文件 - 存储备份(实验性) + 存储备份(测试) 应用程序备份 需要新的恢复代码 新代码 @@ -204,4 +204,22 @@ Calyx Institute 用于 CalyxOS \nNGI0 PET Fund by NLnet 用户选择的位置 + 设备到设备的备份 + 这会强制备份多数应用,即使这些应用不允许备份。此功能处于 alpha 状态,使用后果自负。 + 成功通知 + 下次备份:%1$s + 下次备份(预计):%1$s + 一旦条件满足 + 备份时间安排 + 备份频率 + 每 12 小时 + 每 3 天 + 条件 + 使用移动数据时备份 + 仅充电时备份 + 正在备份 %s 的 APK + 正在保存我们无法备份的应用的列表。 + 当你插入 U 盘时备份会自动发生 + 每天 + 每周 \ No newline at end of file diff --git a/contactsbackup/src/main/res/values-pt/strings.xml b/contactsbackup/src/main/res/values-pt/strings.xml index a6b3daec9..93b477f7a 100644 --- a/contactsbackup/src/main/res/values-pt/strings.xml +++ b/contactsbackup/src/main/res/values-pt/strings.xml @@ -1,2 +1,4 @@ - \ No newline at end of file + + Backup de contatos locais + \ No newline at end of file diff --git a/contactsbackup/src/main/res/values-ro/strings.xml b/contactsbackup/src/main/res/values-ro/strings.xml index a6b3daec9..877c2961a 100644 --- a/contactsbackup/src/main/res/values-ro/strings.xml +++ b/contactsbackup/src/main/res/values-ro/strings.xml @@ -1,2 +1,4 @@ - \ No newline at end of file + + Backup local pentru contacte + \ No newline at end of file diff --git a/storage/lib/src/main/res/values-pt-rBR/strings.xml b/storage/lib/src/main/res/values-pt-rBR/strings.xml index b347a2ad2..9d3884a4a 100644 --- a/storage/lib/src/main/res/values-pt-rBR/strings.xml +++ b/storage/lib/src/main/res/values-pt-rBR/strings.xml @@ -5,18 +5,18 @@ Arquivos de áudio Downloads Adicionar - Backup de armazenamento + Backup do armazenamento Escaneando arquivos… Removendo backups antigos… - Restauração de armazenamento + Restauração do armazenamento %1$d de %2$d arquivos restaurados Nenhum backup de armazenamento encontrado \n -\nDesculpe, mas não há nada que possa ser restaurado. +\nNão há nada que possa ser restaurado. Restaurando arquivos… Backups de armazenamento disponíveis - Fazendo backup de arquivos… + Fazendo backup dos arquivos… %1$d/%2$d - Erro ao carregar instantâneos + Erro ao carregar snapshots Opções \ No newline at end of file diff --git a/storage/lib/src/main/res/values-ro/strings.xml b/storage/lib/src/main/res/values-ro/strings.xml index a6b3daec9..bd11fa339 100644 --- a/storage/lib/src/main/res/values-ro/strings.xml +++ b/storage/lib/src/main/res/values-ro/strings.xml @@ -1,2 +1,22 @@ - \ No newline at end of file + + Opțiuni + Se restaurează fișierele… + %1$d/%2$d + %1$d din %2$d fișiere restaurate + Backup-uri disponibile + Eroare la încărcarea instantaneelor + Descărcări + Backup stocare + Se scanează fișierele… + Se fac copii de rezervă ale fișierelor… + Se elimină vechile copii de rezervă… + Restaurare stocare + Nu s-au găsit copii de rezervă +\n +\nNe pare rău, dar nu există nimic care să poată fi restaurat. + Fotografii și imagini + Videoclipuri + Fișiere audio + Adaugă + \ No newline at end of file diff --git a/storage/lib/src/main/res/values-sv/strings.xml b/storage/lib/src/main/res/values-sv/strings.xml index a6b3daec9..ad62ec4b7 100644 --- a/storage/lib/src/main/res/values-sv/strings.xml +++ b/storage/lib/src/main/res/values-sv/strings.xml @@ -1,2 +1,14 @@ - \ No newline at end of file + + Alternativ + Foton och bilder + Videor + Ljudfiler + Hämtningar + Lägg till + Skannar filer … + Säkerhetskopierar filer … + Tar bort gamla säkerhetskopior … + %1$d/%2$d + %1$d av %2$d filer återställda + \ No newline at end of file From 870d1617d2607dbdad5d6ab9d04f35fdaca05610 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 13 Feb 2024 16:08:51 -0300 Subject: [PATCH 33/76] Initial implementation of WebDavStoragePlugin --- .github/workflows/build.yml | 4 + Android.bp | 4 + app/build.gradle.kts | 3 + app/src/main/AndroidManifest.xml | 3 + .../seedvault/plugins/StoragePlugin.kt | 5 +- .../saf/DocumentsProviderStoragePlugin.kt | 5 +- .../seedvault/plugins/webdav/WebDavConfig.kt | 12 + .../seedvault/plugins/webdav/WebDavModule.kt | 17 + .../plugins/webdav/WebDavStoragePlugin.kt | 297 ++++++++++++++++++ .../plugins/webdav/WebDavStoragePluginTest.kt | 88 ++++++ .../plugins/webdav/WebDavTestConfig.kt | 22 ++ .../seedvault/transport/TransportTest.kt | 9 +- libs/dav4jvm/Android.bp | 17 + libs/dav4jvm/dav4jvm-2.2.1.jar | Bin 0 -> 294557 bytes libs/dav4jvm/okhttp-4.11.0.jar | Bin 0 -> 786969 bytes libs/dav4jvm/okio-jvm-3.7.0.jar | Bin 0 -> 360630 bytes 16 files changed, 481 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavConfig.kt create mode 100644 app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavModule.kt create mode 100644 app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt create mode 100644 app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePluginTest.kt create mode 100644 app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavTestConfig.kt create mode 100644 libs/dav4jvm/Android.bp create mode 100644 libs/dav4jvm/dav4jvm-2.2.1.jar create mode 100644 libs/dav4jvm/okhttp-4.11.0.jar create mode 100644 libs/dav4jvm/okio-jvm-3.7.0.jar diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3f2e54868..347c612ce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,6 +35,10 @@ jobs: cache: 'gradle' - name: Build + env: + NEXTCLOUD_URL: ${{ vars.NEXTCLOUD_URL }} + NEXTCLOUD_USER: ${{ secrets.NEXTCLOUD_USER }} + NEXTCLOUD_PASS: ${{ secrets.NEXTCLOUD_PASS }} run: ./gradlew compileDebugAndroidTestSources check assemble ktlintCheck - name: Upload APKs diff --git a/Android.bp b/Android.bp index d61e977ce..cdf2e5ac9 100644 --- a/Android.bp +++ b/Android.bp @@ -44,6 +44,10 @@ android_app { "seedvault-lib-koin-android", // bip39 "seedvault-lib-kotlin-bip39", + // WebDAV + "seedvault-lib-dav4jvm", + "seedvault-lib-okhttp", + "seedvault-lib-okio", ], manifest: "app/src/main/AndroidManifest.xml", diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ec43fa210..215f0a687 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -159,6 +159,9 @@ dependencies { implementation(fileTree("${rootProject.rootDir}/libs").include("kotlin-bip39-jvm-1.0.6.jar")) + // dav4jvm - later versions of okhttp need kotlin > 1.9.0 + implementation(fileTree("${rootProject.rootDir}/libs/dav4jvm").include("*.jar")) + /** * Test Dependencies (do not concern the AOSP build) */ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f22788148..da3bafa27 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,9 @@ + + + diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt index 53becfacb..e052bf7a8 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt @@ -75,4 +75,7 @@ interface StoragePlugin { } -class EncryptedMetadata(val token: Long, val inputStreamRetriever: () -> InputStream) +class EncryptedMetadata(val token: Long, val inputStreamRetriever: suspend () -> InputStream) + +internal val tokenRegex = Regex("([0-9]{13})") // good until the year 2286 +internal val chunkFolderRegex = Regex("[a-f0-9]{2}") diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt index e8e02baa5..5f2f35109 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt @@ -7,6 +7,8 @@ import androidx.documentfile.provider.DocumentFile import com.stevesoltys.seedvault.getStorageContext import com.stevesoltys.seedvault.plugins.EncryptedMetadata import com.stevesoltys.seedvault.plugins.StoragePlugin +import com.stevesoltys.seedvault.plugins.chunkFolderRegex +import com.stevesoltys.seedvault.plugins.tokenRegex import com.stevesoltys.seedvault.settings.Storage import java.io.FileNotFoundException import java.io.IOException @@ -137,9 +139,6 @@ internal suspend fun getBackups(context: Context, rootDir: DocumentFile): List { WebDavStoragePlugin(androidContext(), WebDavConfig("", "", "")) } + + single { DocumentsStorage(androidContext(), get()) } + @Suppress("Deprecation") + single { DocumentsProviderLegacyPlugin(androidContext(), get()) } +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt new file mode 100644 index 000000000..7a1391fc0 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt @@ -0,0 +1,297 @@ +package com.stevesoltys.seedvault.plugins.webdav + +import android.content.Context +import android.util.Log +import at.bitfire.dav4jvm.BasicDigestAuthHandler +import at.bitfire.dav4jvm.DavCollection +import at.bitfire.dav4jvm.Response.HrefRelation.SELF +import at.bitfire.dav4jvm.exception.NotFoundException +import at.bitfire.dav4jvm.property.DisplayName +import at.bitfire.dav4jvm.property.ResourceType +import at.bitfire.dav4jvm.property.ResourceType.Companion.COLLECTION +import com.stevesoltys.seedvault.plugins.EncryptedMetadata +import com.stevesoltys.seedvault.plugins.StoragePlugin +import com.stevesoltys.seedvault.plugins.chunkFolderRegex +import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA +import com.stevesoltys.seedvault.plugins.saf.FILE_NO_MEDIA +import com.stevesoltys.seedvault.plugins.tokenRegex +import com.stevesoltys.seedvault.settings.Storage +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.RequestBody +import okio.BufferedSink +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.io.PipedInputStream +import java.io.PipedOutputStream +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +private val TAG = WebDavStoragePlugin::class.java.simpleName +const val DEBUG_LOG = true +const val DIRECTORY_ROOT = ".SeedVaultAndroidBackup" + +@OptIn(DelicateCoroutinesApi::class) +@Suppress("BlockingMethodInNonBlockingContext") +internal class WebDavStoragePlugin( + context: Context, + webDavConfig: WebDavConfig, +) : StoragePlugin { + + private val authHandler = BasicDigestAuthHandler( + domain = null, // Optional, to only authenticate against hosts with this domain. + username = webDavConfig.username, + password = webDavConfig.password, + ) + private val okHttpClient = OkHttpClient.Builder() + .followRedirects(false) + .authenticator(authHandler) + .addNetworkInterceptor(authHandler) + .build() + + private val url = "${webDavConfig.url}/$DIRECTORY_ROOT" + + @Throws(IOException::class) + override suspend fun startNewRestoreSet(token: Long) { + try { + val location = "$url/$token".toHttpUrl() + val davCollection = DavCollection(okHttpClient, location) + + val response = suspendCoroutine { cont -> + davCollection.mkCol(null) { response -> + cont.resume(response) + } + } + debugLog { "startNewRestoreSet($token) = $response" } + } catch (e: Exception) { + if (e is IOException) throw e + else throw IOException(e) + } + } + + @Throws(IOException::class) + override suspend fun initializeDevice() { + // TODO does it make sense to delete anything + // when [startNewRestoreSet] is always called first? Maybe unify both calls? + } + + @Throws(IOException::class) + override suspend fun hasData(token: Long, name: String): Boolean { + val location = "$url/$token/$name".toHttpUrl() + val davCollection = DavCollection(okHttpClient, location) + + return try { + val response = suspendCoroutine { cont -> + davCollection.head { response -> + cont.resume(response) + } + } + debugLog { "hasData($token, $name) = $response" } + response.isSuccessful + } catch (e: NotFoundException) { + debugLog { "hasData($token, $name) = $e" } + false + } catch (e: Exception) { + if (e is IOException) throw e + else throw IOException(e) + } + } + + @Throws(IOException::class) + override suspend fun getOutputStream(token: Long, name: String): OutputStream { + return try { + doGetOutputStream(token, name) + } catch (e: Exception) { + if (e is IOException) throw e + else throw IOException("Error getting OutputStream for $token and $name: ", e) + } + } + + @Throws(IOException::class) + private suspend fun doGetOutputStream(token: Long, name: String): OutputStream { + val location = "$url/$token/$name".toHttpUrl() + val davCollection = DavCollection(okHttpClient, location) + + val pipedInputStream = PipedInputStream() + val pipedOutputStream = PipedCloseActionOutputStream(pipedInputStream) + + val body = object : RequestBody() { + override fun contentType() = "application/octet-stream".toMediaType() + override fun writeTo(sink: BufferedSink) { + pipedInputStream.use { inputStream -> + sink.outputStream().use { outputStream -> + inputStream.copyTo(outputStream) + } + } + } + } + val deferred = GlobalScope.async(Dispatchers.IO) { + davCollection.put(body) { response -> + debugLog { "getOutputStream($token, $name) = $response" } + } + } + pipedOutputStream.doOnClose { + runBlocking { // blocking i/o wait + deferred.await() + } + } + return pipedOutputStream + } + + @Throws(IOException::class) + override suspend fun getInputStream(token: Long, name: String): InputStream { + return try { + doGetInputStream(token, name) + } catch (e: Exception) { + if (e is IOException) throw e + else throw IOException("Error getting InputStream for $token and $name: ", e) + } + } + + @Throws(IOException::class) + private fun doGetInputStream(token: Long, name: String): InputStream { + val location = "$url/$token/$name".toHttpUrl() + val davCollection = DavCollection(okHttpClient, location) + + val pipedInputStream = PipedInputStream() + val pipedOutputStream = PipedOutputStream(pipedInputStream) + + GlobalScope.launch(Dispatchers.IO) { + davCollection.get(accept = "", headers = null) { response -> + val inputStream = response.body?.byteStream() + ?: throw IOException("No response body") + debugLog { "getInputStream($token, $name) = $response" } + pipedOutputStream.use { outputStream -> + inputStream.copyTo(outputStream) + } + } + } + return pipedInputStream + } + + @Throws(IOException::class) + override suspend fun removeData(token: Long, name: String) { + val location = "$url/$token/$name".toHttpUrl() + val davCollection = DavCollection(okHttpClient, location) + + try { + val response = suspendCoroutine { cont -> + davCollection.delete { response -> + cont.resume(response) + } + } + debugLog { "removeData($token, $name) = $response" } + } catch (e: Exception) { + if (e is IOException) throw e + else throw IOException(e) + } + } + + @Throws(IOException::class) + override suspend fun hasBackup(storage: Storage): Boolean { + // TODO this requires refactoring + return true + } + + override suspend fun getAvailableBackups(): Sequence? { + return try { + doGetAvailableBackups() + } catch (e: Exception) { + Log.e(TAG, "Error getting available backups: ", e) + null + } + } + + private suspend fun doGetAvailableBackups(): Sequence { + val location = url.toHttpUrl() + val davCollection = DavCollection(okHttpClient, location) + + // get all restore set tokens in root folder + val tokens = ArrayList() + davCollection.propfind( + depth = 2, + reqProp = arrayOf(DisplayName.NAME, ResourceType.NAME), + ) { response, relation -> + debugLog { "getAvailableBackups() = $response" } + // This callback will be called for every file in the folder + if (relation != SELF && !response.isFolder() && response.href.pathSize >= 2 && + response.hrefName() == FILE_BACKUP_METADATA + ) { + val tokenName = response.href.pathSegments[response.href.pathSegments.size - 2] + getTokenOrNull(tokenName)?.let { token -> + tokens.add(token) + } + } + } + val tokenIterator = tokens.iterator() + return generateSequence { + if (!tokenIterator.hasNext()) return@generateSequence null // end sequence + val token = tokenIterator.next() + EncryptedMetadata(token) { + getInputStream(token, FILE_BACKUP_METADATA) + } + } + } + + private fun getTokenOrNull(name: String): Long? { + val looksLikeToken = name.isNotEmpty() && tokenRegex.matches(name) + if (looksLikeToken) { + return try { + name.toLong() + } catch (e: NumberFormatException) { + throw AssertionError(e) // regex must be wrong + } + } + if (isUnexpectedFile(name)) { + Log.w(TAG, "Found invalid backup set folder: $name") + } + return null + } + + private fun isUnexpectedFile(name: String): Boolean { + return name != FILE_NO_MEDIA && + !chunkFolderRegex.matches(name) && + !name.endsWith(".SeedSnap") + } + + private fun Response.isFolder(): Boolean { + return this[ResourceType::class.java]?.types?.contains(COLLECTION) == true + } + + override val providerPackageName: String = context.packageName // 100% built-in plugin + + private class PipedCloseActionOutputStream( + inputStream: PipedInputStream, + ) : PipedOutputStream(inputStream) { + + private var onClose: (() -> Unit)? = null + + @Throws(IOException::class) + override fun close() { + super.close() + try { + onClose?.invoke() + } catch (e: Exception) { + if (e is IOException) throw e + else throw IOException(e) + } + } + + fun doOnClose(function: () -> Unit) { + this.onClose = function + } + } + + private fun debugLog(block: () -> String) { + if (DEBUG_LOG) Log.d(TAG, block()) + } + +} diff --git a/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePluginTest.kt b/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePluginTest.kt new file mode 100644 index 000000000..47578ea28 --- /dev/null +++ b/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePluginTest.kt @@ -0,0 +1,88 @@ +package com.stevesoltys.seedvault.plugins.webdav + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.stevesoltys.seedvault.TestApp +import com.stevesoltys.seedvault.getRandomByteArray +import com.stevesoltys.seedvault.getRandomString +import com.stevesoltys.seedvault.plugins.EncryptedMetadata +import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA +import com.stevesoltys.seedvault.transport.TransportTest +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.assertThrows +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.io.IOException +import kotlin.random.Random + +@RunWith(AndroidJUnit4::class) +@Config( + sdk = [33], // robolectric does not support 34, yet + application = TestApp::class +) +internal class WebDavStoragePluginTest : TransportTest() { + + private val plugin = WebDavStoragePlugin(context, WebDavTestConfig.getConfig()) + + @Test + fun `test restore sets and reading+writing`() = runBlocking { + val token = System.currentTimeMillis() + val metadata = getRandomByteArray() + + // initially, we don't have any backups + assertEquals(emptySet(), plugin.getAvailableBackups()?.toSet()) + + // and no data + assertFalse(plugin.hasData(token, FILE_BACKUP_METADATA)) + + // start a new restore set, initialize it and write out the metadata file + plugin.startNewRestoreSet(token) + plugin.initializeDevice() + plugin.getOutputStream(token, FILE_BACKUP_METADATA).use { + it.write(metadata) + } + try { + // now we have one backup matching our token + val backups = plugin.getAvailableBackups()?.toSet() ?: fail() + assertEquals(1, backups.size) + assertEquals(token, backups.first().token) + + // read back written data + assertArrayEquals( + metadata, + plugin.getInputStream(token, FILE_BACKUP_METADATA).use { it.readAllBytes() }, + ) + + // it has data now + assertTrue(plugin.hasData(token, FILE_BACKUP_METADATA)) + } finally { + // remove data at the end, so consecutive test runs pass + plugin.removeData(token, FILE_BACKUP_METADATA) + } + } + + @Test + fun `test streams for non-existent data`() = runBlocking { + val token = Random.nextLong(System.currentTimeMillis(), 9999999999999) + val file = getRandomString() + + assertFalse(plugin.hasData(token, file)) + + assertThrows { + plugin.getOutputStream(token, file).use { it.write(getRandomByteArray()) } + } + + assertThrows { + plugin.getInputStream(token, file).use { + it.readAllBytes() + } + } + Unit + } + +} diff --git a/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavTestConfig.kt b/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavTestConfig.kt new file mode 100644 index 000000000..e7f5c6743 --- /dev/null +++ b/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavTestConfig.kt @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.plugins.webdav + +import org.junit.Assume.assumeFalse +import org.junit.jupiter.api.Assertions.fail + +object WebDavTestConfig { + + fun getConfig(): WebDavConfig { + assumeFalse(System.getenv("NEXTCLOUD_URL").isNullOrEmpty()) + return WebDavConfig( + url = System.getenv("NEXTCLOUD_URL") ?: fail(), + username = System.getenv("NEXTCLOUD_USER") ?: fail(), + password = System.getenv("NEXTCLOUD_PASS") ?: fail(), + ) + } + +} diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt index 0af1caa2c..b5e58569a 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt @@ -22,6 +22,7 @@ import com.stevesoltys.seedvault.settings.SettingsManager import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.slot import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD import kotlin.random.Random @@ -70,8 +71,14 @@ internal abstract class TransportTest { init { mockkStatic(Log::class) + val logTagSlot = slot() + val logMsgSlot = slot() every { Log.v(any(), any()) } returns 0 - every { Log.d(any(), any()) } returns 0 + every { Log.d(capture(logTagSlot), capture(logMsgSlot)) } answers { + println("${logTagSlot.captured} - ${logMsgSlot.captured}") + 0 + } + every { Log.d(any(), any(), any()) } returns 0 every { Log.i(any(), any()) } returns 0 every { Log.w(any(), ofType(String::class)) } returns 0 every { Log.w(any(), ofType(String::class), any()) } returns 0 diff --git a/libs/dav4jvm/Android.bp b/libs/dav4jvm/Android.bp new file mode 100644 index 000000000..7a4b82d7a --- /dev/null +++ b/libs/dav4jvm/Android.bp @@ -0,0 +1,17 @@ +java_import { + name: "seedvault-lib-dav4jvm", + jars: ["dav4jvm-2.2.1.jar"], + sdk_version: "current", +} + +java_import { + name: "seedvault-lib-okhttp", + jars: ["okhttp-4.11.0.jar"], + sdk_version: "current", +} + +java_import { + name: "seedvault-lib-okio", + jars: ["okio-jvm-3.7.0.jar"], + sdk_version: "current", +} diff --git a/libs/dav4jvm/dav4jvm-2.2.1.jar b/libs/dav4jvm/dav4jvm-2.2.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..582827d4a9eb4ab8ddaae800367f4886121c9205 GIT binary patch literal 294557 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 qh>fa=)`bXz4})3wy8EWEVtCjV;9Fm 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

9+2A=&$7_p(0*ZRUF-CR9c6PVU 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

qr?>rc9}o*8 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�!n@r)m%+@F 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