From fa4c52fb83a6ed477a02a2cb316856529ca35d3d Mon Sep 17 00:00:00 2001
From: Steve Soltys <steve@stevesoltys.com>
Date: Sat, 30 Dec 2023 15:53:35 -0500
Subject: [PATCH] Add experimental support for forcing D2D transfer backups

---
 .github/scripts/run_tests.sh                  |  4 +-
 .github/workflows/test.yml                    |  3 +-
 app/build.gradle.kts                          |  7 ++-
 app/development/scripts/provision_emulator.sh |  2 +-
 .../seedvault/e2e/LargeBackupTestBase.kt      |  3 -
 .../seedvault/e2e/LargeRestoreTestBase.kt     |  4 ++
 .../seedvault/e2e/LargeTestBase.kt            |  6 ++
 .../seedvault/e2e/SeedvaultLargeTest.kt       | 21 +++++--
 .../seedvault/e2e/SeedvaultLargeTestResult.kt |  4 +-
 .../java/com/stevesoltys/seedvault/App.kt     |  4 --
 .../seedvault/metadata/Metadata.kt            |  2 +
 .../seedvault/metadata/MetadataManager.kt     |  4 ++
 .../seedvault/metadata/MetadataModule.kt      |  2 +-
 .../seedvault/metadata/MetadataReader.kt      |  3 +-
 .../seedvault/metadata/MetadataWriter.kt      |  1 +
 .../seedvault/restore/RestorableBackup.kt     |  3 +
 .../seedvault/settings/AppListRetriever.kt    | 13 +++-
 .../settings/ExpertSettingsFragment.kt        | 10 +++
 .../seedvault/settings/SettingsFragment.kt    |  2 +-
 .../seedvault/settings/SettingsManager.kt     | 10 +++
 .../transport/ConfigurableBackupTransport.kt  | 13 ++--
 .../transport/backup/BackupModule.kt          |  1 +
 .../transport/backup/PackageService.kt        | 61 ++++++++++++-------
 .../transport/restore/RestoreCoordinator.kt   | 29 +++++++--
 app/src/main/res/values/strings.xml           |  2 +
 app/src/main/res/xml/settings_expert.xml      |  6 ++
 .../java/com/stevesoltys/seedvault/TestApp.kt |  2 +
 .../seedvault/metadata/MetadataManagerTest.kt | 17 +++++-
 .../seedvault/transport/TransportTest.kt      |  4 ++
 .../restore/RestoreCoordinatorTest.kt         | 11 +++-
 30 files changed, 200 insertions(+), 54 deletions(-)

diff --git a/.github/scripts/run_tests.sh b/.github/scripts/run_tests.sh
index 48a86572..b680a247 100755
--- a/.github/scripts/run_tests.sh
+++ b/.github/scripts/run_tests.sh
@@ -14,8 +14,10 @@ echo "Setting Seedvault transport..."
 sleep 10
 adb shell bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport
 
+D2D_BACKUP_TEST=$1
+
 large_test_exit_code=0
-./gradlew --stacktrace -Pinstrumented_test_size=large :app:connectedAndroidTest || large_test_exit_code=$?
+./gradlew --stacktrace -Pinstrumented_test_size=large -Pd2d_backup_test="$D2D_BACKUP_TEST" :app:connectedAndroidTest || large_test_exit_code=$?
 
 adb pull /sdcard/seedvault_test_results
 
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a624c775..8aeddf71 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -20,6 +20,7 @@ jobs:
       matrix:
         android_target: [ 33, 34 ]
         emulator_type: [ default ]
+        d2d_backup_test: [ true, false ]
     steps:
       - name: Checkout Code
         uses: actions/checkout@v3
@@ -52,7 +53,7 @@ jobs:
             disable-animations: true
             script: |
               ./app/development/scripts/provision_emulator.sh "test" "system-images;android-${{ matrix.android_target }};${{ matrix.emulator_type }};x86_64"
-              ./.github/scripts/run_tests.sh
+              ./.github/scripts/run_tests.sh ${{ matrix.d2d_backup_test }}
 
       - name: Upload test results
         if: always()
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index cb2151cf..52a0e183 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -24,14 +24,17 @@ android {
         targetSdk = libs.versions.targetSdk.get().toInt()
         versionNameSuffix = "-${gitDescribe()}"
         testInstrumentationRunner = "com.stevesoltys.seedvault.KoinInstrumentationTestRunner"
-        testInstrumentationRunnerArguments(mapOf("disableAnalytics" to "true"))
+        testInstrumentationRunnerArguments["disableAnalytics"] = "true"
 
         if (project.hasProperty("instrumented_test_size")) {
             val testSize = project.property("instrumented_test_size").toString()
             println("Instrumented test size: $testSize")
 
-            testInstrumentationRunnerArguments(mapOf("size" to testSize))
+            testInstrumentationRunnerArguments["size"] = testSize
         }
+
+        val d2dBackupTest = project.findProperty("d2d_backup_test")?.toString() ?: "true"
+        testInstrumentationRunnerArguments["d2d_backup_test"] = d2dBackupTest
     }
 
     signingConfigs {
diff --git a/app/development/scripts/provision_emulator.sh b/app/development/scripts/provision_emulator.sh
index fef04b8b..284e7082 100755
--- a/app/development/scripts/provision_emulator.sh
+++ b/app/development/scripts/provision_emulator.sh
@@ -84,7 +84,7 @@ echo "Downloading and extracting test backup to '/sdcard/seedvault_baseline'..."
 
 if [ ! -f backup.tar.gz ]; then
   echo "Downloading test backup..."
-  wget --quiet https://github.com/seedvault-app/seedvault-test-data/releases/download/1/backup.tar.gz
+  wget --quiet https://github.com/seedvault-app/seedvault-test-data/releases/download/3/backup.tar.gz
 fi
 
 $ADB root
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 70f58974..0886c682 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt
@@ -1,6 +1,5 @@
 package com.stevesoltys.seedvault.e2e
 
-import android.app.backup.IBackupManager
 import android.content.pm.PackageInfo
 import android.os.ParcelFileDescriptor
 import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept
@@ -26,8 +25,6 @@ internal interface LargeBackupTestBase : LargeTestBase {
         private const val BACKUP_TIMEOUT = 360 * 1000L
     }
 
-    val backupManager: IBackupManager get() = get()
-
     val spyBackupNotificationManager: BackupNotificationManager get() = get()
 
     val spyFullBackup: FullBackup get() = get()
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt
index 3d74aede..be95877d 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt
@@ -173,6 +173,10 @@ internal interface LargeRestoreTestBase : LargeTestBase {
         coEvery {
             spyFullRestore.initializeState(any(), any(), any(), any())
         } answers {
+            packageName?.let {
+                restoreResult.full[it] = dataIntercept.toByteArray().sha256()
+            }
+
             packageName = arg<PackageInfo>(3).packageName
             dataIntercept = ByteArrayOutputStream()
 
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt
index 86f14a28..69d0cf62 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt
@@ -1,6 +1,7 @@
 package com.stevesoltys.seedvault.e2e
 
 import android.app.UiAutomation
+import android.app.backup.IBackupManager
 import android.content.Context
 import android.content.pm.PackageInfo
 import android.content.pm.PackageManager.PERMISSION_GRANTED
@@ -72,6 +73,8 @@ internal interface LargeTestBase : KoinComponent {
 
     val spyMetadataManager: MetadataManager get() = get()
 
+    val backupManager: IBackupManager get() = get()
+
     val spyRestoreViewModel: RestoreViewModel
         get() = currentRestoreViewModel ?: error("currentRestoreViewModel is null")
 
@@ -79,6 +82,7 @@ internal interface LargeTestBase : KoinComponent {
         get() = currentRestoreStorageViewModel ?: error("currentRestoreStorageViewModel is null")
 
     fun resetApplicationState() {
+        backupManager.setAutoRestore(false)
         settingsManager.setNewToken(null)
         documentsStorage.reset(null)
 
@@ -95,6 +99,7 @@ internal interface LargeTestBase : KoinComponent {
         }
 
         clearDocumentPickerAppData()
+        device.executeShellCommand("rm -R $externalStorageDir/.SeedVaultAndroidBackup")
     }
 
     fun waitUntilIdle() {
@@ -157,6 +162,7 @@ internal interface LargeTestBase : KoinComponent {
 
     fun clearTestBackups() {
         File(testStoragePath).deleteRecursively()
+        File(testVideoPath).deleteRecursively()
     }
 
     fun changeBackupLocation(
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt
index 2d2be5f1..e0e29f10 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt
@@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.e2e
 
 import android.content.pm.PackageManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
 import kotlinx.coroutines.runBlocking
 import org.junit.After
 import org.junit.Before
@@ -40,6 +41,14 @@ internal abstract class SeedvaultLargeTest :
 
         startRecordingTest(keepRecordingScreen, name.methodName)
         restoreBaselineBackup()
+
+        val arguments = InstrumentationRegistry.getArguments()
+
+        if (arguments.getString("d2d_backup_test") == "true") {
+            println("Enabling D2D backups for test")
+
+            settingsManager.setD2dBackupsEnabled(true)
+        }
     }
 
     @After
@@ -63,10 +72,14 @@ internal abstract class SeedvaultLargeTest :
             val extDir = externalStorageDir
 
             device.executeShellCommand("rm -R $extDir/.SeedVaultAndroidBackup")
-            device.executeShellCommand("cp -R $extDir/$BASELINE_BACKUP_FOLDER/" +
-                ".SeedVaultAndroidBackup $extDir")
-            device.executeShellCommand("cp -R $extDir/$BASELINE_BACKUP_FOLDER/" +
-                "recovery-code.txt $extDir")
+            device.executeShellCommand(
+                "cp -R $extDir/$BASELINE_BACKUP_FOLDER/" +
+                    ".SeedVaultAndroidBackup $extDir"
+            )
+            device.executeShellCommand(
+                "cp -R $extDir/$BASELINE_BACKUP_FOLDER/" +
+                    "recovery-code.txt $extDir"
+            )
         }
 
         if (backupFile.exists()) {
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTestResult.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTestResult.kt
index 3223aa52..4c5e3b6c 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTestResult.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTestResult.kt
@@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.e2e
 
 import android.content.pm.PackageInfo
 import com.stevesoltys.seedvault.metadata.PackageMetadata
+import com.stevesoltys.seedvault.restore.AppRestoreResult
 
 /**
  * Contains maps of (package name -> SHA-256 hashes) of application data.
@@ -12,8 +13,9 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
  * For full backups, the mapping is: Map<PackageName, SHA-256>
  * For K/V backups, the mapping is: Map<PackageName, Map<Key, SHA-256>>
  */
-data class SeedvaultLargeTestResult(
+internal data class SeedvaultLargeTestResult(
     val backupResults: Map<String, PackageMetadata?> = emptyMap(),
+    val restoreResults: Map<String, AppRestoreResult?> = emptyMap(),
     val full: MutableMap<String, String>,
     val kv: MutableMap<String, MutableMap<String, String>>,
     val userApps: List<PackageInfo>,
diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt
index 3cdc3365..f3403de7 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/App.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt
@@ -9,7 +9,6 @@ import android.content.pm.PackageManager.PERMISSION_GRANTED
 import android.os.Build
 import android.os.ServiceManager.getService
 import android.os.StrictMode
-import android.os.SystemProperties
 import android.os.UserManager
 import com.stevesoltys.seedvault.crypto.cryptoModule
 import com.stevesoltys.seedvault.header.headerModule
@@ -60,7 +59,6 @@ open class App : Application() {
 
     override fun onCreate() {
         super.onCreate()
-        SystemProperties.set(BACKUP_D2D_PROPERTY, "true")
         startKoin()
         if (isDebugBuild()) {
             StrictMode.setThreadPolicy(
@@ -123,8 +121,6 @@ const val MAGIC_PACKAGE_MANAGER = PACKAGE_MANAGER_SENTINEL
 const val ANCESTRAL_RECORD_KEY = "@ancestral_record@"
 const val GLOBAL_METADATA_KEY = "@meta@"
 
-const val BACKUP_D2D_PROPERTY = "persist.backup.fake-d2d"
-
 // 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/metadata/Metadata.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt
index c4808331..c8ad6aac 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt
@@ -18,6 +18,7 @@ data class BackupMetadata(
     internal val androidVersion: Int = Build.VERSION.SDK_INT,
     internal val androidIncremental: String = Build.VERSION.INCREMENTAL,
     internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}",
+    internal var d2dBackup: Boolean = false,
     internal val packageMetadataMap: PackageMetadataMap = PackageMetadataMap(),
 )
 
@@ -29,6 +30,7 @@ internal const val JSON_METADATA_TIME = "time"
 internal const val JSON_METADATA_SDK_INT = "sdk_int"
 internal const val JSON_METADATA_INCREMENTAL = "incremental"
 internal const val JSON_METADATA_NAME = "name"
+internal const val JSON_METADATA_D2D_BACKUP = "d2d_backup"
 
 enum class PackageState {
     /**
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 98a57f4a..f58a29a1 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt
@@ -17,6 +17,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
 import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
 import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
 import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
+import com.stevesoltys.seedvault.settings.SettingsManager
 import com.stevesoltys.seedvault.transport.backup.isSystemApp
 import java.io.FileNotFoundException
 import java.io.IOException
@@ -35,6 +36,7 @@ internal class MetadataManager(
     private val crypto: Crypto,
     private val metadataWriter: MetadataWriter,
     private val metadataReader: MetadataReader,
+    private val settingsManager: SettingsManager
 ) {
 
     private val uninitializedMetadata = BackupMetadata(token = 0L, salt = "")
@@ -135,6 +137,8 @@ internal class MetadataManager(
         modifyMetadata(metadataOutputStream) {
             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
diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt
index 68c723a4..0d7ed1a2 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt
@@ -4,7 +4,7 @@ import org.koin.android.ext.koin.androidContext
 import org.koin.dsl.module
 
 val metadataModule = module {
-    single { MetadataManager(androidContext(), get(), get(), get(), get()) }
+    single { MetadataManager(androidContext(), get(), get(), get(), get(), get()) }
     single<MetadataWriter> { MetadataWriterImpl(get()) }
     single<MetadataReader> { MetadataReaderImpl(get()) }
 }
diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt
index 382a1757..bbd6df19 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt
@@ -152,7 +152,8 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
                 androidVersion = meta.getInt(JSON_METADATA_SDK_INT),
                 androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL),
                 deviceName = meta.getString(JSON_METADATA_NAME),
-                packageMetadataMap = packageMetadataMap
+                d2dBackup = meta.optBoolean(JSON_METADATA_D2D_BACKUP, false),
+                packageMetadataMap = packageMetadataMap,
             )
         } catch (e: JSONException) {
             throw SecurityException(e)
diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt
index 1359c112..bbed50c7 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt
@@ -35,6 +35,7 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
                 put(JSON_METADATA_SDK_INT, metadata.androidVersion)
                 put(JSON_METADATA_INCREMENTAL, metadata.androidIncremental)
                 put(JSON_METADATA_NAME, metadata.deviceName)
+                put(JSON_METADATA_D2D_BACKUP, metadata.d2dBackup)
             })
         }
         for ((packageName, packageMetadata) in metadata.packageMetadataMap) {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt
index f1c33c16..2c3abb3c 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt
@@ -23,6 +23,9 @@ data class RestorableBackup(val backupMetadata: BackupMetadata) {
     val deviceName: String
         get() = backupMetadata.deviceName
 
+    val d2dBackup: Boolean
+        get() = backupMetadata.d2dBackup
+
     val packageMetadataMap: PackageMetadataMap
         get() = backupMetadata.packageMetadataMap
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt
index 29cb923c..e185b79b 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt
@@ -55,9 +55,16 @@ internal class AppListRetriever(
 
     @WorkerThread
     fun getAppList(): List<AppListItem> {
-        return listOf(AppSectionTitle(R.string.backup_section_system)) + getSpecialApps() +
-            listOf(AppSectionTitle(R.string.backup_section_user)) + getUserApps() +
-            listOf(AppSectionTitle(R.string.backup_section_not_allowed)) + getNotAllowedApps()
+
+        val appListSections = linkedMapOf(
+            AppSectionTitle(R.string.backup_section_system) to getSpecialApps(),
+            AppSectionTitle(R.string.backup_section_user) to getUserApps(),
+            AppSectionTitle(R.string.backup_section_not_allowed) to getNotAllowedApps()
+        ).filter { it.value.isNotEmpty() }
+
+        return appListSections.flatMap { (sectionTitle, appList) ->
+            listOf(sectionTitle) + appList
+        }
     }
 
     private fun getSpecialApps(): List<AppListItem> {
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 f4a2effd..05607375 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt
@@ -4,6 +4,7 @@ import android.os.Bundle
 import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
 import androidx.preference.Preference
 import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.SwitchPreferenceCompat
 import com.stevesoltys.seedvault.R
 import com.stevesoltys.seedvault.permitDiskReads
 import com.stevesoltys.seedvault.transport.backup.PackageService
@@ -14,6 +15,7 @@ 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)
@@ -23,6 +25,7 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() {
         permitDiskReads {
             setPreferencesFromResource(R.xml.settings_expert, rootKey)
         }
+
         findPreference<Preference>("logcat")?.setOnPreferenceClickListener {
             val versionName = packageService.getVersionName(requireContext().packageName) ?: "ver"
             val timestamp = System.currentTimeMillis()
@@ -30,6 +33,13 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() {
             createFileLauncher.launch(name)
             true
         }
+
+        val d2dPreference = findPreference<SwitchPreferenceCompat>(PREF_KEY_D2D_BACKUPS)
+
+        d2dPreference?.setOnPreferenceChangeListener { _, newValue ->
+            d2dPreference.isChecked = newValue as Boolean
+            true
+        }
     }
 
     override fun onStart() {
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 93ed0883..38aaf805 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt
@@ -89,7 +89,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
             true
         }
 
-        autoRestore = findPreference("auto_restore")!!
+        autoRestore = findPreference(PREF_KEY_AUTO_RESTORE)!!
         autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
             val enabled = newValue as Boolean
             try {
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 a83c844f..2647e57b 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt
@@ -16,6 +16,7 @@ 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"
 
 private const val PREF_KEY_STORAGE_URI = "storageUri"
 private const val PREF_KEY_STORAGE_NAME = "storageName"
@@ -31,6 +32,7 @@ private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist"
 
 private const val PREF_KEY_BACKUP_STORAGE = "backup_storage"
 private const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota"
+internal const val PREF_KEY_D2D_BACKUPS = "d2d_backups"
 
 class SettingsManager(private val context: Context) {
 
@@ -151,6 +153,14 @@ class SettingsManager(private val context: Context) {
     }
 
     fun isQuotaUnlimited() = prefs.getBoolean(PREF_KEY_UNLIMITED_QUOTA, false)
+
+    fun d2dBackupsEnabled() = prefs.getBoolean(PREF_KEY_D2D_BACKUPS, false)
+
+    fun setD2dBackupsEnabled(enabled: Boolean) {
+        prefs.edit()
+            .putBoolean(PREF_KEY_D2D_BACKUPS, enabled)
+            .apply()
+    }
 }
 
 data class Storage(
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt
index a90b14b0..cead1eab 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt
@@ -12,6 +12,7 @@ import android.os.ParcelFileDescriptor
 import android.util.Log
 import com.stevesoltys.seedvault.R
 import com.stevesoltys.seedvault.settings.SettingsActivity
+import com.stevesoltys.seedvault.settings.SettingsManager
 import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
 import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
 import kotlinx.coroutines.runBlocking
@@ -21,9 +22,8 @@ import org.koin.core.component.inject
 // If we ever change this, we should use a ComponentName like the other backup transports.
 val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name
 
-// Since there seems to be consensus in the community to pose as device-to-device transport,
-// we are pretending to be one here. This will back up opt-out apps that target at least API 31.
-const val TRANSPORT_FLAGS = FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED or FLAG_DEVICE_TO_DEVICE_TRANSFER
+const val DEFAULT_TRANSPORT_FLAGS = FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
+const val D2D_TRANSPORT_FLAGS = DEFAULT_TRANSPORT_FLAGS or FLAG_DEVICE_TO_DEVICE_TRANSFER
 
 private const val TRANSPORT_DIRECTORY_NAME =
     "com.stevesoltys.seedvault.transport.ConfigurableBackupTransport"
@@ -38,6 +38,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
 
     private val backupCoordinator by inject<BackupCoordinator>()
     private val restoreCoordinator by inject<RestoreCoordinator>()
+    private val settingsManager by inject<SettingsManager>()
 
     override fun transportDirName(): String {
         return TRANSPORT_DIRECTORY_NAME
@@ -57,7 +58,11 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
      * This allows the agent to decide what to do based on properties of the transport.
      */
     override fun getTransportFlags(): Int {
-        return TRANSPORT_FLAGS
+        return if (settingsManager.d2dBackupsEnabled()) {
+            D2D_TRANSPORT_FLAGS
+        } else {
+            DEFAULT_TRANSPORT_FLAGS
+        }
     }
 
     /**
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 14fd6261..bf7d3272 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
@@ -8,6 +8,7 @@ val backupModule = module {
     single {
         PackageService(
             context = androidContext(),
+            backupManager = get(),
             settingsManager = get(),
             plugin = get()
         )
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt
index d298856e..ca2e8ab0 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt
@@ -1,5 +1,6 @@
 package com.stevesoltys.seedvault.transport.backup
 
+import android.app.backup.IBackupManager
 import android.content.Context
 import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
 import android.content.pm.ApplicationInfo.FLAG_STOPPED
@@ -11,6 +12,7 @@ import android.content.pm.PackageManager
 import android.content.pm.PackageManager.GET_INSTRUMENTATION
 import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
 import android.os.RemoteException
+import android.os.UserHandle
 import android.util.Log
 import android.util.Log.INFO
 import androidx.annotation.WorkerThread
@@ -28,11 +30,13 @@ private const val LOG_MAX_PACKAGES = 100
  */
 internal class PackageService(
     private val context: Context,
+    private val backupManager: IBackupManager,
     private val settingsManager: SettingsManager,
     private val plugin: StoragePlugin,
 ) {
 
     private val packageManager: PackageManager = context.packageManager
+    private val myUserId = UserHandle.myUserId()
 
     val eligiblePackages: Array<String>
         @WorkerThread
@@ -48,13 +52,7 @@ internal class PackageService(
                 logPackages(packages)
             }
 
-            // We do not use BackupManager.filterAppsEligibleForBackupForUser because it
-            // always makes its determinations based on OperationType.BACKUP, never based on
-            // OperationType.MIGRATION, and there are no alternative publicly-available APIs.
-            // We don't need to use it, here, either; during a backup or migration, the system
-            // will perform its own eligibility checks regardless. We merely need to filter out
-            // apps that we, or the user, want to exclude.
-            val eligibleApps = packages.filter(::shouldIncludeAppInBackup)
+            val eligibleApps = packages.filter(::shouldIncludeAppInBackup).toTypedArray()
 
             // log eligible packages
             if (Log.isLoggable(TAG, INFO)) {
@@ -97,7 +95,8 @@ internal class PackageService(
     val userApps: List<PackageInfo>
         @WorkerThread
         get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo ->
-            packageInfo.isUserVisible(context) && packageInfo.allowsBackup()
+            packageInfo.isUserVisible(context) &&
+                packageInfo.allowsBackup(settingsManager.d2dBackupsEnabled())
         }
 
     /**
@@ -106,7 +105,8 @@ internal class PackageService(
     val userNotAllowedApps: List<PackageInfo>
         @WorkerThread
         get() = packageManager.getInstalledPackages(0).filter { packageInfo ->
-            !packageInfo.allowsBackup() && !packageInfo.isSystemApp()
+            !packageInfo.allowsBackup(settingsManager.d2dBackupsEnabled()) &&
+                !packageInfo.isSystemApp()
         }
 
     val expectedAppTotals: ExpectedAppTotals
@@ -132,11 +132,24 @@ internal class PackageService(
     }
 
     fun shouldIncludeAppInBackup(packageName: String): Boolean {
+        // We do not use BackupManager.filterAppsEligibleForBackupForUser for D2D because it
+        // always makes its determinations based on OperationType.BACKUP, never based on
+        // OperationType.MIGRATION, and there are no alternative publicly-available APIs.
+        // We don't need to use it, here, either; during a backup or migration, the system
+        // will perform its own eligibility checks regardless. We merely need to filter out
+        // apps that we, or the user, want to exclude.
+
         // Check that the app is not excluded by user preference
         val enabled = settingsManager.isBackupEnabled(packageName)
-        // We also need to exclude the DocumentsProvider used to store backup data.
-        // Otherwise, it gets killed when we back it up, terminating our backup.
-        return enabled && packageName != plugin.providerPackageName
+
+        // We need to explicitly exclude DocumentsProvider and Seedvault.
+        // Otherwise, they get killed while backing them up, terminating our backup.
+        val excludedPackages = setOf(
+            plugin.providerPackageName,
+            context.packageName
+        )
+
+        return enabled && !excludedPackages.contains(packageName)
     }
 
     private fun logPackages(packages: List<String>) {
@@ -168,18 +181,24 @@ internal fun PackageInfo.isSystemApp(): Boolean {
     return applicationInfo.flags and FLAG_SYSTEM != 0
 }
 
-internal fun PackageInfo.allowsBackup(): Boolean {
+internal fun PackageInfo.allowsBackup(d2dBackup: Boolean): Boolean {
     if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
 
-    // TODO: Consider ways of replicating the system's logic so that the user can have advance
-    // knowledge of apps that the system will exclude, particularly apps targeting SDK 30 or below.
+    return if (d2dBackup) {
+        // TODO: Consider ways of replicating the system's logic so that the user can have advance
+        // knowledge of apps that the system will exclude, particularly apps targeting SDK 30 or
+        // below.
 
-    // At backup time, the system will filter out any apps that *it* does not want to be backed up.
-    // Now that we have switched to D2D, *we* generally want to back up as much as possible;
-    // part of the point of D2D is to ignore FLAG_ALLOW_BACKUP (allowsBackup). So, we return true.
-    // See frameworks/base/services/backup/java/com/android/server/backup/utils/
-    // BackupEligibilityRules.java lines 74-81 and 163-167 (tag: android-13.0.0_r8).
-    return true
+        // At backup time, the system will filter out any apps that *it* does not want to be
+        // backed up. If the user has enabled D2D, *we* generally want to back up as much as
+        // possible; part of the point of D2D is to ignore FLAG_ALLOW_BACKUP (allowsBackup).
+        // So, we return true.
+        // See frameworks/base/services/backup/java/com/android/server/backup/utils/
+        // BackupEligibilityRules.java lines 74-81 and 163-167 (tag: android-13.0.0_r8).
+        true
+    } else {
+        applicationInfo.flags and FLAG_ALLOW_BACKUP != 0
+    }
 }
 
 /**
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt
index f6a00ecc..68431258 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt
@@ -22,7 +22,8 @@ import com.stevesoltys.seedvault.metadata.MetadataManager
 import com.stevesoltys.seedvault.metadata.MetadataReader
 import com.stevesoltys.seedvault.plugins.StoragePlugin
 import com.stevesoltys.seedvault.settings.SettingsManager
-import com.stevesoltys.seedvault.transport.TRANSPORT_FLAGS
+import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
+import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import java.io.IOException
 
@@ -32,7 +33,7 @@ import java.io.IOException
  * backup/restore/ActiveRestoreSession.java. AOSP currently relies on this constant, and it is not
  * publicly exposed. Framework code indicates they intend to use a flag, instead, in the future.
  */
-internal const val DEVICE_NAME_FOR_D2D_SET = "D2D"
+internal const val D2D_DEVICE_NAME = "D2D"
 
 private data class RestoreCoordinatorState(
     val token: Long,
@@ -100,8 +101,20 @@ internal class RestoreCoordinator(
      **/
     suspend fun getAvailableRestoreSets(): Array<RestoreSet>? {
         return getAvailableMetadata()?.map { (_, metadata) ->
-            RestoreSet(metadata.deviceName /* name */, DEVICE_NAME_FOR_D2D_SET /* device */,
-                metadata.token, TRANSPORT_FLAGS)
+
+            val transportFlags = if (metadata.d2dBackup) {
+                D2D_TRANSPORT_FLAGS
+            } else {
+                DEFAULT_TRANSPORT_FLAGS
+            }
+
+            val deviceName = if (metadata.d2dBackup) {
+                D2D_DEVICE_NAME
+            } else {
+                metadata.deviceName
+            }
+
+            RestoreSet(metadata.deviceName, deviceName, metadata.token, transportFlags)
         }?.toTypedArray()
     }
 
@@ -123,6 +136,10 @@ internal class RestoreCoordinator(
      */
     fun beforeStartRestore(backupMetadata: BackupMetadata) {
         this.backupMetadata = backupMetadata
+
+        if (backupMetadata.d2dBackup) {
+            settingsManager.setD2dBackupsEnabled(true)
+        }
     }
 
     /**
@@ -228,6 +245,7 @@ internal class RestoreCoordinator(
                         TYPE_KEY_VALUE
                     } else throw IOException("No data found for $packageName. Skipping.")
                 }
+
                 BackupType.FULL -> {
                     val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
                     if (plugin.hasData(state.token, name)) {
@@ -237,6 +255,7 @@ internal class RestoreCoordinator(
                         TYPE_FULL_STREAM
                     } else throw IOException("No data found for $packageName. Skipping...")
                 }
+
                 null -> {
                     Log.i(TAG, "No backup type found for $packageName. Skipping...")
                     state.backupMetadata.packageMetadataMap[packageName]?.backupType?.let { s ->
@@ -270,12 +289,14 @@ internal class RestoreCoordinator(
                     state.currentPackage = packageName
                     TYPE_KEY_VALUE
                 }
+
                 full.hasDataForPackage(state.token, packageInfo) -> {
                     Log.i(TAG, "Found full backup data for $packageName.")
                     full.initializeState(0x00, state.token, "", packageInfo)
                     state.currentPackage = packageName
                     TYPE_FULL_STREAM
                 }
+
                 else -> {
                     Log.i(TAG, "No data found for $packageName. Skipping.")
                     return nextRestorePackage()
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 502ba39c..18f8326f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -49,6 +49,8 @@
     <string name="settings_expert_title">Expert settings</string>
     <string name="settings_expert_quota_title">Unlimited app quota</string>
     <string name="settings_expert_quota_summary">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.</string>
+    <string name="settings_expert_d2d_title">Device-to-device backups</string>
+    <string name="settings_expert_d2d_summary">Tell AOSP that our backups are being used for a D2D transfer. This forces backups for most apps, even when they disallow them.\n\nWarning: This is experimental, use at your own risk.\n\nSee more info on our FAQ.</string>
     <string name="settings_expert_logcat_title">Save app log</string>
     <string name="settings_expert_logcat_summary">Developers can diagnose bugs with these logs.\n\nWarning: The log file might contain personally identifiable information. Review before and delete after sharing!</string>
     <string name="settings_expert_logcat_error">Error: Could not save app log</string>
diff --git a/app/src/main/res/xml/settings_expert.xml b/app/src/main/res/xml/settings_expert.xml
index 11e8497c..0125bf4e 100644
--- a/app/src/main/res/xml/settings_expert.xml
+++ b/app/src/main/res/xml/settings_expert.xml
@@ -5,6 +5,12 @@
         android:key="unlimited_quota"
         android:summary="@string/settings_expert_quota_summary"
         android:title="@string/settings_expert_quota_title" />
+    <SwitchPreferenceCompat
+        android:id="@+id/d2d_backup_preference"
+        android:defaultValue="false"
+        android:key="d2d_backups"
+        android:summary="@string/settings_expert_d2d_summary"
+        android:title="@string/settings_expert_d2d_title" />
     <Preference
         android:icon="@drawable/ic_bug_report"
         android:key="logcat"
diff --git a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt
index b1ad45a5..cdf03aea 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt
@@ -10,6 +10,7 @@ import com.stevesoltys.seedvault.header.headerModule
 import com.stevesoltys.seedvault.metadata.metadataModule
 import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
 import com.stevesoltys.seedvault.restore.install.installModule
+import com.stevesoltys.seedvault.settings.SettingsManager
 import com.stevesoltys.seedvault.transport.backup.backupModule
 import com.stevesoltys.seedvault.transport.restore.restoreModule
 import org.koin.android.ext.koin.androidContext
@@ -25,6 +26,7 @@ class TestApp : App() {
     }
     private val appModule = module {
         single { Clock() }
+        single { SettingsManager(this@TestApp) }
     }
 
     override fun startKoin() = startKoin {
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 661677ab..521aac65 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt
@@ -19,6 +19,7 @@ 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.settings.SettingsManager
 import io.mockk.Runs
 import io.mockk.every
 import io.mockk.just
@@ -27,6 +28,7 @@ import io.mockk.verify
 import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Assert.fail
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.koin.core.context.stopKoin
@@ -51,8 +53,16 @@ class MetadataManagerTest {
     private val crypto: Crypto = mockk()
     private val metadataWriter: MetadataWriter = mockk()
     private val metadataReader: MetadataReader = mockk()
+    private val settingsManager: SettingsManager = mockk()
 
-    private val manager = MetadataManager(context, clock, crypto, metadataWriter, metadataReader)
+    private val manager = MetadataManager(
+        context = context,
+        clock = clock,
+        crypto = crypto,
+        metadataWriter = metadataWriter,
+        metadataReader = metadataReader,
+        settingsManager = settingsManager
+    )
 
     private val time = 42L
     private val token = Random.nextLong()
@@ -69,6 +79,11 @@ class MetadataManagerTest {
     private val cacheInputStream: FileInputStream = mockk()
     private val encodedMetadata = getRandomByteArray()
 
+    @Before
+    fun beforeEachTest() {
+        every { settingsManager.d2dBackupsEnabled() } returns false
+    }
+
     @After
     fun afterEachTest() {
         stopKoin()
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 35e81b28..0af1caa2 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt
@@ -59,6 +59,10 @@ internal abstract class TransportTest {
             put(packageInfo.packageName, PackageMetadata(backupType = BackupType.KV))
         }
     )
+    protected val d2dMetadata = metadata.copy(
+        d2dBackup = true
+    )
+
     protected val salt = metadata.salt
     protected val name = getRandomString(12)
     protected val name2 = getRandomString(23)
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt
index 4f704dee..88ba4c15 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt
@@ -87,9 +87,18 @@ internal class RestoreCoordinatorTest : TransportTest() {
 
         val sets = restore.getAvailableRestoreSets() ?: fail()
         assertEquals(2, sets.size)
-        assertEquals(DEVICE_NAME_FOR_D2D_SET, sets[0].device)
+        assertEquals(metadata.deviceName, sets[0].device)
         assertEquals(metadata.deviceName, sets[0].name)
         assertEquals(metadata.token, sets[0].token)
+
+        every { metadataReader.readMetadata(inputStream, token) } returns d2dMetadata
+        every { metadataReader.readMetadata(inputStream, token + 1) } returns d2dMetadata
+
+        val d2dSets = restore.getAvailableRestoreSets() ?: fail()
+        assertEquals(2, d2dSets.size)
+        assertEquals(D2D_DEVICE_NAME, d2dSets[0].device)
+        assertEquals(metadata.deviceName, d2dSets[0].name)
+        assertEquals(metadata.token, d2dSets[0].token)
     }
 
     @Test