diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 5edad4fd..665b7acc 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,11 +1,6 @@
 <component name="ProjectCodeStyleConfiguration">
   <code_scheme name="Project" version="173">
     <JetCodeStyleSettings>
-      <option name="PACKAGES_TO_USE_STAR_IMPORTS">
-        <value />
-      </option>
-      <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
-      <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
       <option name="ALLOW_TRAILING_COMMA" value="true" />
       <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
     </JetCodeStyleSettings>
diff --git a/.idea/copyright/Apache_2_0.xml b/.idea/copyright/Apache_2_0.xml
deleted file mode 100644
index b0f0e1ab..00000000
--- a/.idea/copyright/Apache_2_0.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<component name="CopyrightManager">
-  <copyright>
-    <option name="notice" value="SPDX-FileCopyrightText: &amp;#36;today.year The Calyx Institute&#10;SPDX-License-Identifier: Apache-2.0" />
-    <option name="myName" value="Apache-2.0" />
-  </copyright>
-</component>
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt
index d3eff96d..6a27ed23 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt
@@ -1,12 +1,12 @@
 package com.stevesoltys.seedvault
 
-import com.stevesoltys.seedvault.restore.RestoreViewModel
-import com.stevesoltys.seedvault.transport.backup.FullBackup
-import com.stevesoltys.seedvault.transport.backup.InputFactory
-import com.stevesoltys.seedvault.transport.backup.KVBackup
-import com.stevesoltys.seedvault.transport.restore.FullRestore
-import com.stevesoltys.seedvault.transport.restore.KVRestore
-import com.stevesoltys.seedvault.transport.restore.OutputFactory
+import com.stevesoltys.seedvault.ui.restore.RestoreViewModel
+import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
+import com.stevesoltys.seedvault.service.app.backup.InputFactory
+import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
+import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
+import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
+import com.stevesoltys.seedvault.service.app.restore.OutputFactory
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
 import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
@@ -26,8 +26,8 @@ class KoinInstrumentationTestApp : App() {
             val context = this@KoinInstrumentationTestApp
 
             single { spyk(BackupNotificationManager(context)) }
-            single { spyk(FullBackup(get(), get(), get(), get())) }
-            single { spyk(KVBackup(get(), get(), get(), get(), get())) }
+            single { spyk(FullBackupService(get(), get(), get(), get())) }
+            single { spyk(KVBackupService(get(), get(), get(), get(), get())) }
             single { spyk(InputFactory()) }
 
             single { spyk(FullRestore(get(), get(), get(), get(), get())) }
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
index f5dddd70..e8a158fc 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
@@ -4,14 +4,14 @@ import androidx.test.core.content.pm.PackageInfoBuilder
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry
-import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
-import com.stevesoltys.seedvault.plugins.StoragePlugin
-import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderLegacyPlugin
-import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderStoragePlugin
-import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
-import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
-import com.stevesoltys.seedvault.plugins.saf.deleteContents
-import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.storage.saf.legacy.DocumentsProviderLegacyPlugin
+import com.stevesoltys.seedvault.service.storage.saf.DocumentsProviderStoragePlugin
+import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
+import com.stevesoltys.seedvault.service.storage.saf.FILE_BACKUP_METADATA
+import com.stevesoltys.seedvault.service.storage.saf.deleteContents
+import com.stevesoltys.seedvault.service.settings.SettingsService
 import io.mockk.every
 import io.mockk.mockk
 import kotlinx.coroutines.Dispatchers
@@ -33,9 +33,9 @@ import org.koin.core.component.inject
 class PluginTest : KoinComponent {
 
     private val context = InstrumentationRegistry.getInstrumentation().targetContext
-    private val settingsManager: SettingsManager by inject()
-    private val mockedSettingsManager: SettingsManager = mockk()
-    private val storage = DocumentsStorage(context, mockedSettingsManager)
+    private val settingsService: SettingsService by inject()
+    private val mockedSettingsService: SettingsService = mockk()
+    private val storage = DocumentsStorage(context, mockedSettingsService)
 
     private val storagePlugin: StoragePlugin = DocumentsProviderStoragePlugin(context, storage)
 
@@ -49,7 +49,7 @@ class PluginTest : KoinComponent {
 
     @Before
     fun setup() = runBlocking {
-        every { mockedSettingsManager.getStorage() } returns settingsManager.getStorage()
+        every { mockedSettingsService.getStorage() } returns settingsService.getStorage()
         storage.rootBackupDir?.deleteContents(context)
             ?: error("Select a storage location in the app first!")
     }
@@ -76,11 +76,11 @@ class PluginTest : KoinComponent {
     fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
         // no backups available initially
         assertEquals(0, storagePlugin.getAvailableBackups()?.toList()?.size)
-        val s = settingsManager.getStorage() ?: error("no storage")
+        val s = settingsService.getStorage() ?: error("no storage")
         assertFalse(storagePlugin.hasBackup(s))
 
         // prepare returned tokens requested when initializing device
-        every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1)
+        every { mockedSettingsService.getToken() } returnsMany listOf(token, token + 1, token + 1)
 
         // start new restore set and initialize device afterwards
         storagePlugin.startNewRestoreSet(token)
@@ -114,7 +114,7 @@ class PluginTest : KoinComponent {
 
     @Test
     fun testMetadataWriteRead() = runBlocking(Dispatchers.IO) {
-        every { mockedSettingsManager.getToken() } returns token
+        every { mockedSettingsService.getToken() } returns token
 
         storagePlugin.startNewRestoreSet(token)
         storagePlugin.initializeDevice()
@@ -216,7 +216,7 @@ class PluginTest : KoinComponent {
     }
 
     private fun initStorage(token: Long) = runBlocking {
-        every { mockedSettingsManager.getToken() } returns token
+        every { mockedSettingsService.getToken() } returns token
         storagePlugin.initializeDevice()
     }
 
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 0886c682..3d7f275e 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt
@@ -5,9 +5,9 @@ import android.os.ParcelFileDescriptor
 import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept
 import com.stevesoltys.seedvault.e2e.io.InputStreamIntercept
 import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
-import com.stevesoltys.seedvault.transport.backup.FullBackup
-import com.stevesoltys.seedvault.transport.backup.InputFactory
-import com.stevesoltys.seedvault.transport.backup.KVBackup
+import com.stevesoltys.seedvault.service.app.backup.InputFactory
+import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
+import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import io.mockk.clearMocks
 import io.mockk.coEvery
@@ -27,9 +27,9 @@ internal interface LargeBackupTestBase : LargeTestBase {
 
     val spyBackupNotificationManager: BackupNotificationManager get() = get()
 
-    val spyFullBackup: FullBackup get() = get()
+    val spyFullBackupService: FullBackupService get() = get()
 
-    val spyKVBackup: KVBackup get() = get()
+    val spyKVBackupService: KVBackupService get() = get()
 
     val spyInputFactory: InputFactory get() = get()
 
@@ -72,7 +72,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
 
         return backupResult.copy(
             backupResults = backupResult.allUserApps().associate {
-                it.packageName to spyMetadataManager.getPackageMetadata(it.packageName)
+                it.packageName to spyMetadataService.getPackageMetadata(it.packageName)
             }.toMutableMap()
         )
     }
@@ -88,7 +88,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
     }
 
     private fun spyOnBackup(backupResult: SeedvaultLargeTestResult): AtomicBoolean {
-        clearMocks(spyInputFactory, spyKVBackup, spyFullBackup)
+        clearMocks(spyInputFactory, spyKVBackupService, spyFullBackupService)
         spyOnFullBackupData(backupResult)
         spyOnKVBackupData(backupResult)
 
@@ -100,7 +100,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
         var data = mutableMapOf<String, ByteArray>()
 
         coEvery {
-            spyKVBackup.performBackup(any(), any(), any(), any(), any())
+            spyKVBackupService.performBackup(any(), any(), any(), any(), any())
         } answers {
             packageName = firstArg<PackageInfo>().packageName
             callOriginal()
@@ -117,7 +117,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
         }
 
         coEvery {
-            spyKVBackup.finishBackup()
+            spyKVBackupService.finishBackup()
         } answers {
             backupResult.kv[packageName!!] = data
                 .mapValues { entry -> entry.value.sha256() }
@@ -134,7 +134,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
         var dataIntercept = ByteArrayOutputStream()
 
         coEvery {
-            spyFullBackup.performFullBackup(any(), any(), any(), any(), any())
+            spyFullBackupService.performFullBackup(any(), any(), any(), any(), any())
         } answers {
             packageName = firstArg<PackageInfo>().packageName
             callOriginal()
@@ -150,7 +150,7 @@ internal interface LargeBackupTestBase : LargeTestBase {
         }
 
         every {
-            spyFullBackup.finishBackup()
+            spyFullBackupService.finishBackup()
         } answers {
             val result = callOriginal()
             backupResult.full[packageName!!] = dataIntercept.toByteArray().sha256()
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 be95877d..37df98a3 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt
@@ -6,9 +6,9 @@ import com.stevesoltys.seedvault.e2e.io.BackupDataOutputIntercept
 import com.stevesoltys.seedvault.e2e.io.OutputStreamIntercept
 import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen
 import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen
-import com.stevesoltys.seedvault.transport.restore.FullRestore
-import com.stevesoltys.seedvault.transport.restore.KVRestore
-import com.stevesoltys.seedvault.transport.restore.OutputFactory
+import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
+import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
+import com.stevesoltys.seedvault.service.app.restore.OutputFactory
 import io.mockk.clearMocks
 import io.mockk.coEvery
 import io.mockk.every
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 69d0cf62..968d7f6c 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt
@@ -11,21 +11,21 @@ import androidx.preference.PreferenceManager
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.UiDevice
 import androidx.test.uiautomator.Until
-import com.stevesoltys.seedvault.crypto.ANDROID_KEY_STORE
-import com.stevesoltys.seedvault.crypto.KEY_ALIAS_BACKUP
-import com.stevesoltys.seedvault.crypto.KEY_ALIAS_MAIN
-import com.stevesoltys.seedvault.crypto.KeyManager
+import com.stevesoltys.seedvault.service.crypto.ANDROID_KEY_STORE
+import com.stevesoltys.seedvault.service.crypto.KEY_ALIAS_BACKUP
+import com.stevesoltys.seedvault.service.crypto.KEY_ALIAS_MAIN
+import com.stevesoltys.seedvault.service.crypto.KeyManager
 import com.stevesoltys.seedvault.currentRestoreStorageViewModel
 import com.stevesoltys.seedvault.currentRestoreViewModel
 import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
 import com.stevesoltys.seedvault.e2e.screen.impl.DocumentPickerScreen
 import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen
-import com.stevesoltys.seedvault.metadata.MetadataManager
+import com.stevesoltys.seedvault.service.metadata.MetadataService
 import com.stevesoltys.seedvault.permitDiskReads
-import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
-import com.stevesoltys.seedvault.restore.RestoreViewModel
-import com.stevesoltys.seedvault.settings.SettingsManager
-import com.stevesoltys.seedvault.transport.backup.PackageService
+import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
+import com.stevesoltys.seedvault.ui.restore.RestoreViewModel
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.service.app.PackageService
 import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
 import kotlinx.coroutines.DelicateCoroutinesApi
 import kotlinx.coroutines.Dispatchers
@@ -65,13 +65,13 @@ internal interface LargeTestBase : KoinComponent {
 
     val packageService: PackageService get() = get()
 
-    val settingsManager: SettingsManager get() = get()
+    val settingsService: SettingsService get() = get()
 
     val keyManager: KeyManager get() = get()
 
     val documentsStorage: DocumentsStorage get() = get()
 
-    val spyMetadataManager: MetadataManager get() = get()
+    val spyMetadataService: MetadataService get() = get()
 
     val backupManager: IBackupManager get() = get()
 
@@ -83,7 +83,7 @@ internal interface LargeTestBase : KoinComponent {
 
     fun resetApplicationState() {
         backupManager.setAutoRestore(false)
-        settingsManager.setNewToken(null)
+        settingsService.setNewToken(null)
         documentsStorage.reset(null)
 
         val sharedPreferences = permitDiskReads {
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 e0e29f10..b20cb184 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt
@@ -47,7 +47,7 @@ internal abstract class SeedvaultLargeTest :
         if (arguments.getString("d2d_backup_test") == "true") {
             println("Enabling D2D backups for test")
 
-            settingsManager.setD2dBackupsEnabled(true)
+            settingsService.setD2dBackupsEnabled(true)
         }
     }
 
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 4c5e3b6c..b41dc61e 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTestResult.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTestResult.kt
@@ -1,8 +1,8 @@
 package com.stevesoltys.seedvault.e2e
 
 import android.content.pm.PackageInfo
-import com.stevesoltys.seedvault.metadata.PackageMetadata
-import com.stevesoltys.seedvault.restore.AppRestoreResult
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.ui.restore.AppRestoreResult
 
 /**
  * Contains maps of (package name -> SHA-256 hashes) of application data.
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/impl/BackupRestoreTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/impl/BackupRestoreTest.kt
index 83e638b6..a528b21d 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/impl/BackupRestoreTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/impl/BackupRestoreTest.kt
@@ -3,7 +3,7 @@ package com.stevesoltys.seedvault.e2e.impl
 import androidx.test.filters.LargeTest
 import com.stevesoltys.seedvault.e2e.SeedvaultLargeTest
 import com.stevesoltys.seedvault.e2e.SeedvaultLargeTestResult
-import com.stevesoltys.seedvault.metadata.PackageState
+import com.stevesoltys.seedvault.service.metadata.PackageState
 import org.junit.Test
 
 @LargeTest
@@ -17,7 +17,7 @@ internal class BackupRestoreTest : SeedvaultLargeTest() {
             confirmCode()
         }
 
-        if (settingsManager.getStorage() == null) {
+        if (settingsService.getStorage() == null) {
             chooseStorageLocation()
         } else {
             changeBackupLocation()
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt
index 353d7680..8b6a1461 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt
@@ -13,7 +13,11 @@ import com.stevesoltys.seedvault.assertReadEquals
 import com.stevesoltys.seedvault.coAssertThrows
 import com.stevesoltys.seedvault.getRandomBase64
 import com.stevesoltys.seedvault.getRandomByteArray
-import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
+import com.stevesoltys.seedvault.service.storage.saf.createOrGetFile
+import com.stevesoltys.seedvault.service.storage.saf.findFileBlocking
+import com.stevesoltys.seedvault.service.storage.saf.getLoadedCursor
+import com.stevesoltys.seedvault.service.settings.SettingsService
 import com.stevesoltys.seedvault.writeAndClose
 import io.mockk.Runs
 import io.mockk.every
@@ -44,8 +48,8 @@ import kotlin.random.Random
 class DocumentsStorageTest : KoinComponent {
 
     private val context = InstrumentationRegistry.getInstrumentation().targetContext
-    private val settingsManager by inject<SettingsManager>()
-    private val storage = DocumentsStorage(context, settingsManager)
+    private val settingsService by inject<SettingsService>()
+    private val storage = DocumentsStorage(context, settingsService)
 
     private val filename = getRandomBase64()
     private lateinit var file: DocumentFile
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt
index f140eb22..8ecad933 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt
@@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.transport.backup
 import android.util.Log
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import com.stevesoltys.seedvault.service.app.PackageService
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.koin.core.component.KoinComponent
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
index 0888d2f4..19305302 100644
--- a/app/src/debug/AndroidManifest.xml
+++ b/app/src/debug/AndroidManifest.xml
@@ -5,11 +5,11 @@
     <application>
         <!-- Remove permission requirements only for debug versions to make development easier -->
         <activity
-            android:name="com.stevesoltys.seedvault.settings.SettingsActivity"
+            android:name="com.stevesoltys.seedvault.ui.settings.SettingsActivity"
             android:exported="true"
             tools:remove="android:permission" />
         <activity
-            android:name="com.stevesoltys.seedvault.restore.RestoreActivity"
+            android:name="com.stevesoltys.seedvault.ui.restore.RestoreActivity"
             android:exported="true"
             tools:remove="android:permission" />
     </application>
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 57c91c08..98bbc430 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -87,7 +87,7 @@
         tools:ignore="GoogleAppIndexingWarning">
 
         <activity
-            android:name=".settings.SettingsActivity"
+            android:name=".ui.settings.SettingsActivity"
             android:exported="true"
             android:permission="com.stevesoltys.seedvault.OPEN_SETTINGS" />
 
@@ -106,7 +106,7 @@
             android:theme="@style/AppTheme.NoActionBar" />
 
         <activity
-            android:name=".restore.RestoreActivity"
+            android:name=".ui.restore.RestoreActivity"
             android:exported="true"
             android:label="@string/restore_title"
             android:permission="com.stevesoltys.seedvault.RESTORE_BACKUP"
@@ -137,7 +137,7 @@
         </receiver>
 
         <receiver
-            android:name=".restore.RestoreErrorBroadcastReceiver"
+            android:name=".ui.restore.RestoreErrorBroadcastReceiver"
             android:exported="false">
             <intent-filter>
                 <action android:name="com.stevesoltys.seedvault.action.UNINSTALL" />
@@ -145,7 +145,7 @@
         </receiver>
 
         <receiver
-            android:name=".SecretCodeReceiver"
+            android:name=".ui.restore.SecretCodeReceiver"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.telephony.action.SECRET_CODE" />
@@ -158,19 +158,19 @@
 
         <!-- Used to start actual BackupService depending on scheduling criteria -->
         <service
-            android:name=".storage.StorageBackupJobService"
+            android:name=".service.file.backup.FileBackupJobService"
             android:exported="false"
             android:label="BackupJobService"
             android:permission="android.permission.BIND_JOB_SERVICE" />
         <!-- Does the actual backup work as a foreground service -->
         <service
-            android:name=".storage.StorageBackupService"
+            android:name=".service.file.backup.FileBackupService"
             android:exported="false"
             android:foregroundServiceType="dataSync"
             android:label="BackupService" />
         <!-- Does restore as a foreground service -->
         <service
-            android:name=".storage.StorageRestoreService"
+            android:name=".service.file.restore.FileRestoreService"
             android:exported="false"
             android:foregroundServiceType="dataSync"
             android:label="RestoreService" />
diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt
index f3403de7..9f1d6460 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/App.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt
@@ -10,24 +10,25 @@ import android.os.Build
 import android.os.ServiceManager.getService
 import android.os.StrictMode
 import android.os.UserManager
-import com.stevesoltys.seedvault.crypto.cryptoModule
-import com.stevesoltys.seedvault.header.headerModule
-import com.stevesoltys.seedvault.metadata.MetadataManager
-import com.stevesoltys.seedvault.metadata.metadataModule
-import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
-import com.stevesoltys.seedvault.restore.RestoreViewModel
-import com.stevesoltys.seedvault.restore.install.installModule
-import com.stevesoltys.seedvault.settings.AppListRetriever
-import com.stevesoltys.seedvault.settings.SettingsManager
-import com.stevesoltys.seedvault.settings.SettingsViewModel
-import com.stevesoltys.seedvault.storage.storageModule
-import com.stevesoltys.seedvault.transport.backup.backupModule
-import com.stevesoltys.seedvault.transport.restore.restoreModule
+import com.stevesoltys.seedvault.service.crypto.cryptoModule
+import com.stevesoltys.seedvault.service.header.headerModule
+import com.stevesoltys.seedvault.service.metadata.MetadataService
+import com.stevesoltys.seedvault.service.metadata.metadataModule
+import com.stevesoltys.seedvault.service.storage.saf.documentsProviderModule
+import com.stevesoltys.seedvault.ui.restore.RestoreViewModel
+import com.stevesoltys.seedvault.ui.restore.apk.installModule
+import com.stevesoltys.seedvault.ui.settings.AppListRetriever
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.ui.settings.SettingsViewModel
+import com.stevesoltys.seedvault.service.file.filesModule
+import com.stevesoltys.seedvault.service.app.backup.backupModule
+import com.stevesoltys.seedvault.service.app.restore.restoreModule
 import com.stevesoltys.seedvault.ui.files.FileSelectionViewModel
 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.util.TimeSource
 import org.koin.android.ext.android.inject
 import org.koin.android.ext.koin.androidContext
 import org.koin.android.ext.koin.androidLogger
@@ -43,9 +44,9 @@ import org.koin.dsl.module
 open class App : Application() {
 
     private val appModule = module {
-        single { SettingsManager(this@App) }
+        single { SettingsService(this@App) }
         single { BackupNotificationManager(this@App) }
-        single { Clock() }
+        single { TimeSource() }
         factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
         factory { AppListRetriever(this@App, get(), get(), get()) }
 
@@ -94,24 +95,24 @@ open class App : Application() {
         backupModule,
         restoreModule,
         installModule,
-        storageModule,
+        filesModule,
         appModule
     )
 
-    private val settingsManager: SettingsManager by inject()
-    private val metadataManager: MetadataManager by inject()
+    private val settingsService: SettingsService by inject()
+    private val metadataService: MetadataService by inject()
 
     /**
-     * The responsibility for the current token was moved to the [SettingsManager]
+     * The responsibility for the current token was moved to the [SettingsService]
      * in the end of 2020.
      * This method migrates the token for existing installs and can be removed
      * after sufficient time has passed.
      */
     private fun migrateTokenFromMetadataToSettingsManager() {
         @Suppress("DEPRECATION")
-        val token = metadataManager.getBackupToken()
-        if (token != 0L && settingsManager.getToken() == null) {
-            settingsManager.setNewToken(token)
+        val token = metadataService.getBackupToken()
+        if (token != 0L && settingsService.getToken() == null) {
+            settingsService.setNewToken(token)
         }
     }
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt
index 4800fcef..2e02a432 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt
@@ -15,12 +15,12 @@ import android.os.Looper
 import android.provider.DocumentsContract
 import android.util.Log
 import androidx.core.content.ContextCompat.startForegroundService
-import com.stevesoltys.seedvault.metadata.MetadataManager
-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.service.app.backup.requestBackup
+import com.stevesoltys.seedvault.service.file.backup.FileBackupService
+import com.stevesoltys.seedvault.service.file.backup.FileBackupService.Companion.EXTRA_START_APP_BACKUP
+import com.stevesoltys.seedvault.service.metadata.MetadataService
+import com.stevesoltys.seedvault.service.settings.FlashDrive
+import com.stevesoltys.seedvault.service.settings.SettingsService
 import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE
 import org.koin.core.context.GlobalContext.get
 import java.util.concurrent.TimeUnit.HOURS
@@ -32,17 +32,17 @@ private const val HOURS_AUTO_BACKUP: Long = 24
 class UsbIntentReceiver : UsbMonitor() {
 
     // using KoinComponent would crash robolectric tests :(
-    private val settingsManager: SettingsManager by lazy { get().get() }
-    private val metadataManager: MetadataManager by lazy { get().get() }
+    private val settingsService: SettingsService by lazy { get().get() }
+    private val metadataService: MetadataService by lazy { get().get() }
 
     override fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean {
         if (action != ACTION_USB_DEVICE_ATTACHED) return false
         Log.d(TAG, "Checking if this is the current backup drive.")
-        val savedFlashDrive = settingsManager.getFlashDrive() ?: return false
+        val savedFlashDrive = settingsService.getFlashDrive() ?: return false
         val attachedFlashDrive = FlashDrive.from(device)
         return if (savedFlashDrive == attachedFlashDrive) {
             Log.d(TAG, "Matches stored device, checking backup time...")
-            val backupMillis = System.currentTimeMillis() - metadataManager.getLastBackupTime()
+            val backupMillis = System.currentTimeMillis() - metadataService.getLastBackupTime()
             if (backupMillis >= HOURS.toMillis(HOURS_AUTO_BACKUP)) {
                 Log.d(TAG, "Last backup older than 24 hours, requesting a backup...")
                 true
@@ -57,8 +57,8 @@ class UsbIntentReceiver : UsbMonitor() {
     }
 
     override fun onStatusChanged(context: Context, action: String, device: UsbDevice) {
-        if (settingsManager.isStorageBackupEnabled()) {
-            val i = Intent(context, StorageBackupService::class.java)
+        if (settingsService.isStorageBackupEnabled()) {
+            val i = Intent(context, FileBackupService::class.java)
             // this starts an app backup afterwards
             i.putExtra(EXTRA_START_APP_BACKUP, true)
             startForegroundService(context, i)
diff --git a/app/src/main/java/com/stevesoltys/seedvault/header/HeaderModule.kt b/app/src/main/java/com/stevesoltys/seedvault/header/HeaderModule.kt
deleted file mode 100644
index 82d5a04d..00000000
--- a/app/src/main/java/com/stevesoltys/seedvault/header/HeaderModule.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.stevesoltys.seedvault.header
-
-import org.koin.dsl.module
-
-val headerModule = module {
-    single<HeaderReader> { HeaderReaderImpl() }
-}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/BackupMonitor.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/BackupManagerOperationMonitor.kt
similarity index 78%
rename from app/src/main/java/com/stevesoltys/seedvault/BackupMonitor.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/app/BackupManagerOperationMonitor.kt
index 8e961916..5a1af5fc 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/BackupMonitor.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/BackupManagerOperationMonitor.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault
+package com.stevesoltys.seedvault.service.app
 
 import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY
 import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_ID
@@ -8,9 +8,9 @@ import android.os.Bundle
 import android.util.Log
 import android.util.Log.DEBUG
 
-private val TAG = BackupMonitor::class.java.name
+private val TAG = BackupManagerOperationMonitor::class.java.name
 
-class BackupMonitor : IBackupManagerMonitor.Stub() {
+class BackupManagerOperationMonitor : IBackupManagerMonitor.Stub() {
 
     override fun onEvent(bundle: Bundle) {
         if (!Log.isLoggable(TAG, DEBUG)) return
@@ -18,5 +18,4 @@ class BackupMonitor : IBackupManagerMonitor.Stub() {
         Log.d(TAG, "CATEGORY: " + bundle.getInt(EXTRA_LOG_EVENT_CATEGORY, -1))
         Log.d(TAG, "PACKAGE: " + bundle.getString(EXTRA_LOG_EVENT_PACKAGE_NAME, "?"))
     }
-
 }
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/PackageService.kt
similarity index 92%
rename from app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/app/PackageService.kt
index ca2e8ab0..4daadebb 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/PackageService.kt
@@ -1,6 +1,5 @@
-package com.stevesoltys.seedvault.transport.backup
+package com.stevesoltys.seedvault.service.app
 
-import android.app.backup.IBackupManager
 import android.content.Context
 import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
 import android.content.pm.ApplicationInfo.FLAG_STOPPED
@@ -12,17 +11,12 @@ 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
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
-import com.stevesoltys.seedvault.plugins.StoragePlugin
-import com.stevesoltys.seedvault.settings.SettingsManager
-
-private val TAG = PackageService::class.java.simpleName
-
-private const val LOG_MAX_PACKAGES = 100
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.settings.SettingsService
 
 /**
  * @author Steve Soltys
@@ -30,13 +24,17 @@ private const val LOG_MAX_PACKAGES = 100
  */
 internal class PackageService(
     private val context: Context,
-    private val backupManager: IBackupManager,
-    private val settingsManager: SettingsManager,
+    private val settingsService: SettingsService,
     private val plugin: StoragePlugin,
 ) {
 
+    companion object {
+        private val TAG = PackageService::class.java.simpleName
+
+        private const val LOG_MAX_PACKAGES = 100
+    }
+
     private val packageManager: PackageManager = context.packageManager
-    private val myUserId = UserHandle.myUserId()
 
     val eligiblePackages: Array<String>
         @WorkerThread
@@ -96,7 +94,7 @@ internal class PackageService(
         @WorkerThread
         get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo ->
             packageInfo.isUserVisible(context) &&
-                packageInfo.allowsBackup(settingsManager.d2dBackupsEnabled())
+                packageInfo.allowsBackup(settingsService.d2dBackupsEnabled())
         }
 
     /**
@@ -105,7 +103,7 @@ internal class PackageService(
     val userNotAllowedApps: List<PackageInfo>
         @WorkerThread
         get() = packageManager.getInstalledPackages(0).filter { packageInfo ->
-            !packageInfo.allowsBackup(settingsManager.d2dBackupsEnabled()) &&
+            !packageInfo.allowsBackup(settingsService.d2dBackupsEnabled()) &&
                 !packageInfo.isSystemApp()
         }
 
@@ -140,7 +138,7 @@ internal class PackageService(
         // apps that we, or the user, want to exclude.
 
         // Check that the app is not excluded by user preference
-        val enabled = settingsManager.isBackupEnabled(packageName)
+        val enabled = settingsService.isBackupEnabled(packageName)
 
         // We need to explicitly exclude DocumentsProvider and Seedvault.
         // Otherwise, they get killed while backing them up, terminating our backup.
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/AppBackupService.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/AppBackupService.kt
new file mode 100644
index 00000000..4d329c9f
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/AppBackupService.kt
@@ -0,0 +1,54 @@
+package com.stevesoltys.seedvault.service.app.backup
+
+import android.app.backup.BackupManager
+import android.app.backup.IBackupManager
+import android.content.Context
+import android.os.RemoteException
+import android.util.Log
+import androidx.annotation.WorkerThread
+import com.stevesoltys.seedvault.service.app.BackupManagerOperationMonitor
+import com.stevesoltys.seedvault.service.app.PackageService
+import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
+import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver
+import org.koin.core.context.GlobalContext
+
+private val TAG = AppBackupService::class.java.simpleName
+
+internal class AppBackupService(
+    private val context: Context,
+) {
+
+    fun initiateBackup() {
+        requestBackup(context)
+    }
+}
+
+/**
+ * TODO: Move to above service class.
+ */
+@WorkerThread
+fun requestBackup(context: Context) {
+    val backupManager: IBackupManager = GlobalContext.get().get()
+    if (backupManager.isBackupEnabled) {
+        val packageService: PackageService = GlobalContext.get().get()
+        val packages = packageService.eligiblePackages
+        val appTotals = packageService.expectedAppTotals
+
+        val result = try {
+            Log.d(TAG, "Backup is enabled, request backup...")
+            val observer = NotificationBackupObserver(context, packages.size, appTotals)
+            backupManager.requestBackup(packages, observer, BackupManagerOperationMonitor(), 0)
+        } catch (e: RemoteException) {
+            Log.e(TAG, "Error during backup: ", e)
+            val nm: BackupNotificationManager = GlobalContext.get().get()
+            nm.onBackupError()
+        }
+        if (result == BackupManager.SUCCESS) {
+            Log.i(TAG, "Backup succeeded ")
+        } else {
+            Log.e(TAG, "Backup failed: $result")
+        }
+    } else {
+        Log.i(TAG, "Backup is not enabled")
+    }
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/BackupModule.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/BackupModule.kt
new file mode 100644
index 00000000..681da237
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/BackupModule.kt
@@ -0,0 +1,62 @@
+package com.stevesoltys.seedvault.service.app.backup
+
+import com.stevesoltys.seedvault.service.app.PackageService
+import com.stevesoltys.seedvault.service.app.backup.apk.ApkBackupService
+import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
+import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
+import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
+import com.stevesoltys.seedvault.service.app.backup.kv.KvDbManager
+import com.stevesoltys.seedvault.service.app.backup.kv.KvDbManagerImpl
+import org.koin.android.ext.koin.androidContext
+import org.koin.dsl.module
+
+val backupModule = module {
+    single { InputFactory() }
+    single {
+        PackageService(
+            context = androidContext(),
+            settingsService = get(),
+            plugin = get()
+        )
+    }
+    single {
+        ApkBackupService(
+            pm = androidContext().packageManager,
+            cryptoService = get(),
+            settingsService = get(),
+            metadataService = get()
+        )
+    }
+    single<KvDbManager> { KvDbManagerImpl(androidContext()) }
+    single {
+        KVBackupService(
+            plugin = get(),
+            settingsService = get(),
+            inputFactory = get(),
+            cryptoService = get(),
+            dbManager = get()
+        )
+    }
+    single {
+        FullBackupService(
+            plugin = get(),
+            settingsService = get(),
+            inputFactory = get(),
+            cryptoService = get()
+        )
+    }
+    single {
+        BackupCoordinatorService(
+            context = androidContext(),
+            plugin = get(),
+            kv = get(),
+            full = get(),
+            apkBackupService = get(),
+            timeSource = get(),
+            packageService = get(),
+            metadataService = get(),
+            settingsService = get(),
+            nm = get()
+        )
+    }
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/InputFactory.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/InputFactory.kt
similarity index 91%
rename from app/src/main/java/com/stevesoltys/seedvault/transport/backup/InputFactory.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/app/backup/InputFactory.kt
index 87ba7bc0..f06deecf 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/InputFactory.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/InputFactory.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.transport.backup
+package com.stevesoltys.seedvault.service.app.backup
 
 import android.app.backup.BackupDataInput
 import android.os.ParcelFileDescriptor
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/ApkBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/apk/ApkBackupService.kt
similarity index 88%
rename from app/src/main/java/com/stevesoltys/seedvault/transport/backup/ApkBackup.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/app/backup/apk/ApkBackupService.kt
index 02208eb0..71a09306 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/ApkBackup.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/apk/ApkBackupService.kt
@@ -1,4 +1,12 @@
-package com.stevesoltys.seedvault.transport.backup
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.stevesoltys.seedvault.service.app.backup.apk
 
 import android.annotation.SuppressLint
 import android.content.pm.PackageInfo
@@ -8,13 +16,15 @@ import android.content.pm.SigningInfo
 import android.util.Log
 import android.util.PackageUtils.computeSha256DigestBytes
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
-import com.stevesoltys.seedvault.crypto.Crypto
-import com.stevesoltys.seedvault.encodeBase64
-import com.stevesoltys.seedvault.metadata.ApkSplit
-import com.stevesoltys.seedvault.metadata.MetadataManager
-import com.stevesoltys.seedvault.metadata.PackageMetadata
-import com.stevesoltys.seedvault.metadata.PackageState
-import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.service.crypto.CryptoService
+import com.stevesoltys.seedvault.util.encodeBase64
+import com.stevesoltys.seedvault.service.metadata.ApkSplit
+import com.stevesoltys.seedvault.service.metadata.MetadataService
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.metadata.PackageState
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.service.app.isNotUpdatedSystemApp
+import com.stevesoltys.seedvault.service.app.isTestOnly
 import java.io.File
 import java.io.FileInputStream
 import java.io.FileNotFoundException
@@ -23,14 +33,14 @@ import java.io.InputStream
 import java.io.OutputStream
 import java.security.MessageDigest
 
-private val TAG = ApkBackup::class.java.simpleName
+private val TAG = ApkBackupService::class.java.simpleName
 
 @Suppress("BlockingMethodInNonBlockingContext")
-internal class ApkBackup(
+internal class ApkBackupService(
     private val pm: PackageManager,
-    private val crypto: Crypto,
-    private val settingsManager: SettingsManager,
-    private val metadataManager: MetadataManager,
+    private val cryptoService: CryptoService,
+    private val settingsService: SettingsService,
+    private val metadataService: MetadataService,
 ) {
 
     /**
@@ -53,7 +63,7 @@ internal class ApkBackup(
         if (packageName == MAGIC_PACKAGE_MANAGER) return null
 
         // do not back up when setting is not enabled
-        if (!settingsManager.backupApks()) return null
+        if (!settingsService.backupApks()) return null
 
         // do not back up test-only apps as we can't re-install them anyway
         // see: https://commonsware.com/blog/2017/10/31/android-studio-3p0-flag-test-only.html
@@ -82,7 +92,7 @@ internal class ApkBackup(
         }
 
         // get cached metadata about package
-        val packageMetadata = metadataManager.getPackageMetadata(packageName)
+        val packageMetadata = metadataService.getPackageMetadata(packageName)
             ?: PackageMetadata()
 
         // get version codes
@@ -104,7 +114,7 @@ internal class ApkBackup(
         // get an InputStream for the APK
         val inputStream = getApkInputStream(packageInfo.applicationInfo.sourceDir)
         // copy the APK to the storage's output and calculate SHA-256 hash while at it
-        val name = crypto.getNameForApk(metadataManager.salt, packageName)
+        val name = cryptoService.getNameForApk(metadataService.salt, packageName)
         val sha256 = copyStreamsAndGetHash(inputStream, streamGetter(name))
 
         // back up splits if they exist
@@ -195,7 +205,7 @@ internal class ApkBackup(
             }
         }
         val sha256 = messageDigest.digest().encodeBase64()
-        val name = crypto.getNameForApk(metadataManager.salt, packageName, splitName)
+        val name = cryptoService.getNameForApk(metadataService.salt, packageName, splitName)
         // copy the split APK to the storage stream
         getApkInputStream(sourceDir).use { inputStream ->
             streamGetter(name).use { outputStream ->
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/coordinator/BackupCoordinatorService.kt
similarity index 86%
rename from app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/app/backup/coordinator/BackupCoordinatorService.kt
index 87a13020..9e38402d 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/coordinator/BackupCoordinatorService.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.transport.backup
+package com.stevesoltys.seedvault.service.app.backup.coordinator
 
 import android.app.backup.BackupTransport.FLAG_DATA_NOT_CHANGED
 import android.app.backup.BackupTransport.FLAG_INCREMENTAL
@@ -18,62 +18,52 @@ import android.os.ParcelFileDescriptor
 import android.util.Log
 import androidx.annotation.VisibleForTesting
 import androidx.annotation.WorkerThread
-import com.stevesoltys.seedvault.Clock
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
-import com.stevesoltys.seedvault.metadata.BackupType
-import com.stevesoltys.seedvault.metadata.MetadataManager
-import com.stevesoltys.seedvault.metadata.PackageState
-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.SettingsManager
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.app.PackageService
+import com.stevesoltys.seedvault.service.app.backup.apk.ApkBackupService
+import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
+import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
+import com.stevesoltys.seedvault.service.app.isStopped
+import com.stevesoltys.seedvault.service.app.isSystemApp
+import com.stevesoltys.seedvault.service.metadata.BackupType
+import com.stevesoltys.seedvault.service.metadata.MetadataService
+import com.stevesoltys.seedvault.service.metadata.PackageState
+import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
+import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.QUOTA_EXCEEDED
+import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
+import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
+import com.stevesoltys.seedvault.service.settings.SettingsService
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
+import com.stevesoltys.seedvault.util.TimeSource
 import java.io.IOException
 import java.io.OutputStream
 import java.util.concurrent.TimeUnit.DAYS
 import java.util.concurrent.TimeUnit.HOURS
 
-private val TAG = BackupCoordinator::class.java.simpleName
-
-private class CoordinatorState(
-    var calledInitialize: Boolean,
-    var calledClearBackupData: Boolean,
-    var cancelReason: PackageState,
-) {
-    val expectFinish: Boolean
-        get() = calledInitialize || calledClearBackupData
-
-    fun onFinish() {
-        calledInitialize = false
-        calledClearBackupData = false
-        cancelReason = UNKNOWN_ERROR
-    }
-}
-
 /**
  * @author Steve Soltys
  * @author Torsten Grote
  */
 @WorkerThread // entire class should always be accessed from a worker thread, so blocking is ok
 @Suppress("BlockingMethodInNonBlockingContext")
-internal class BackupCoordinator(
+internal class BackupCoordinatorService(
     private val context: Context,
     private val plugin: StoragePlugin,
-    private val kv: KVBackup,
-    private val full: FullBackup,
-    private val apkBackup: ApkBackup,
-    private val clock: Clock,
+    private val kv: KVBackupService,
+    private val full: FullBackupService,
+    private val apkBackupService: ApkBackupService,
+    private val timeSource: TimeSource,
     private val packageService: PackageService,
-    private val metadataManager: MetadataManager,
-    private val settingsManager: SettingsManager,
+    private val metadataService: MetadataService,
+    private val settingsService: SettingsService,
     private val nm: BackupNotificationManager,
 ) {
 
-    private val state = CoordinatorState(
+    private val TAG = BackupCoordinatorService::class.java.simpleName
+
+    private val state = BackupCoordinatorState(
         calledInitialize = false,
         calledClearBackupData = false,
         cancelReason = UNKNOWN_ERROR
@@ -92,9 +82,9 @@ internal class BackupCoordinator(
      */
     @Throws(IOException::class)
     private suspend fun startNewRestoreSet(): Long {
-        val token = clock.time()
+        val token = timeSource.time()
         Log.i(TAG, "Starting new RestoreSet with token $token...")
-        settingsManager.setNewToken(token)
+        settingsService.setNewToken(token)
         plugin.startNewRestoreSet(token)
         return token
     }
@@ -125,7 +115,7 @@ internal class BackupCoordinator(
         plugin.initializeDevice()
         Log.d(TAG, "Resetting backup metadata for token $token...")
         plugin.getMetadataOutputStream(token).use {
-            metadataManager.onDeviceInitialization(token, it)
+            metadataService.onDeviceInitialization(token, it)
         }
         // [finishBackup] will only be called when we return [TRANSPORT_OK] here
         // so we remember that we initialized successfully
@@ -134,7 +124,7 @@ internal class BackupCoordinator(
     } catch (e: IOException) {
         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()
+        if (metadataService.requiresInit || settingsService.canDoBackupNow()) nm.onBackupError()
         TRANSPORT_ERROR
     }
 
@@ -230,7 +220,7 @@ internal class BackupCoordinator(
         flags: Int,
     ): Int {
         state.cancelReason = UNKNOWN_ERROR
-        if (metadataManager.requiresInit) {
+        if (metadataService.requiresInit) {
             // start a new restore set to upgrade from legacy format
             // by starting a clean backup with all files using the new version
             try {
@@ -241,8 +231,8 @@ internal class BackupCoordinator(
             // 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")
-        val salt = metadataManager.salt
+        val token = settingsService.getToken() ?: error("no token in performFullBackup")
+        val salt = metadataService.salt
         return kv.performBackup(packageInfo, data, flags, token, salt)
     }
 
@@ -280,8 +270,8 @@ internal class BackupCoordinator(
         flags: Int,
     ): Int {
         state.cancelReason = UNKNOWN_ERROR
-        val token = settingsManager.getToken() ?: error("no token in performFullBackup")
-        val salt = metadataManager.salt
+        val token = settingsService.getToken() ?: error("no token in performFullBackup")
+        val salt = metadataService.salt
         return full.performFullBackup(targetPackage, fileDescriptor, flags, token, salt)
     }
 
@@ -310,8 +300,8 @@ internal class BackupCoordinator(
         // don't bother with system apps that have no data
         val ignoreApp = state.cancelReason == NO_DATA && packageInfo.isSystemApp()
         if (!ignoreApp) onPackageBackupError(packageInfo, BackupType.FULL)
-        val token = settingsManager.getToken() ?: error("no token in cancelFullBackup")
-        val salt = metadataManager.salt
+        val token = settingsService.getToken() ?: error("no token in cancelFullBackup")
+        val salt = metadataService.salt
         full.cancelFullBackup(token, salt, ignoreApp)
     }
 
@@ -329,8 +319,8 @@ internal class BackupCoordinator(
     suspend fun clearBackupData(packageInfo: PackageInfo): Int {
         val packageName = packageInfo.packageName
         Log.i(TAG, "Clear Backup Data of $packageName.")
-        val token = settingsManager.getToken() ?: error("no token in clearBackupData")
-        val salt = metadataManager.salt
+        val token = settingsService.getToken() ?: error("no token in clearBackupData")
+        val salt = metadataService.salt
         try {
             kv.clearBackupData(packageInfo, token, salt)
         } catch (e: IOException) {
@@ -368,7 +358,7 @@ internal class BackupCoordinator(
             if (result == TRANSPORT_OK) {
                 val isPmBackup = packageName == MAGIC_PACKAGE_MANAGER
                 // call onPackageBackedUp for @pm@ only if we can do backups right now
-                if (!isPmBackup || settingsManager.canDoBackupNow()) {
+                if (!isPmBackup || settingsService.canDoBackupNow()) {
                     try {
                         onPackageBackedUp(packageInfo, BackupType.KV)
                     } catch (e: Exception) {
@@ -377,7 +367,7 @@ internal class BackupCoordinator(
                     }
                 }
                 // hook in here to back up APKs of apps that are otherwise not allowed for backup
-                if (isPmBackup && settingsManager.canDoBackupNow()) {
+                if (isPmBackup && settingsService.canDoBackupNow()) {
                     try {
                         backUpApksOfNotBackedUpPackages()
                     } catch (e: Exception) {
@@ -426,7 +416,7 @@ internal class BackupCoordinator(
                 val wasBackedUp = backUpApk(packageInfo, packageState)
                 if (!wasBackedUp) {
                     val packageMetadata =
-                        metadataManager.getPackageMetadata(packageName)
+                        metadataService.getPackageMetadata(packageName)
                     val oldPackageState = packageMetadata?.state
                     if (oldPackageState != packageState) {
                         Log.i(
@@ -434,7 +424,7 @@ internal class BackupCoordinator(
                                 ", update to $packageState"
                         )
                         plugin.getMetadataOutputStream().use {
-                            metadataManager.onPackageBackupError(packageInfo, packageState, it)
+                            metadataService.onPackageBackupError(packageInfo, packageState, it)
                         }
                     }
                 }
@@ -455,12 +445,12 @@ internal class BackupCoordinator(
     ): Boolean {
         val packageName = packageInfo.packageName
         return try {
-            apkBackup.backupApkIfNecessary(packageInfo, packageState) { name ->
-                val token = settingsManager.getToken() ?: throw IOException("no current token")
+            apkBackupService.backupApkIfNecessary(packageInfo, packageState) { name ->
+                val token = settingsService.getToken() ?: throw IOException("no current token")
                 plugin.getOutputStream(token, name)
             }?.let { packageMetadata ->
                 plugin.getMetadataOutputStream().use {
-                    metadataManager.onApkBackedUp(packageInfo, packageMetadata, it)
+                    metadataService.onApkBackedUp(packageInfo, packageMetadata, it)
                 }
                 true
             } ?: false
@@ -472,7 +462,7 @@ internal class BackupCoordinator(
 
     private suspend fun onPackageBackedUp(packageInfo: PackageInfo, type: BackupType) {
         plugin.getMetadataOutputStream().use {
-            metadataManager.onPackageBackedUp(packageInfo, type, it)
+            metadataService.onPackageBackedUp(packageInfo, type, it)
         }
     }
 
@@ -480,7 +470,7 @@ internal class BackupCoordinator(
         val packageName = packageInfo.packageName
         try {
             plugin.getMetadataOutputStream().use {
-                metadataManager.onPackageBackupError(packageInfo, state.cancelReason, it, type)
+                metadataService.onPackageBackupError(packageInfo, state.cancelReason, it, type)
             }
         } catch (e: IOException) {
             Log.e(TAG, "Error while writing metadata for $packageName", e)
@@ -491,7 +481,7 @@ internal class BackupCoordinator(
         val longBackoff = DAYS.toMillis(30)
 
         // back off if there's no storage set
-        val storage = settingsManager.getStorage() ?: return longBackoff
+        val storage = settingsService.getStorage() ?: return longBackoff
         return when {
             // back off if storage is removable and not available right now
             storage.isUnavailableUsb(context) -> longBackoff
@@ -503,8 +493,10 @@ internal class BackupCoordinator(
     }
 
     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)
+        val t = token ?: settingsService.getToken() ?: throw IOException("no current token")
+        return getOutputStream(t,
+            com.stevesoltys.seedvault.service.storage.saf.FILE_BACKUP_METADATA
+        )
     }
 
 }
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/coordinator/BackupCoordinatorState.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/coordinator/BackupCoordinatorState.kt
new file mode 100644
index 00000000..9bd623ca
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/coordinator/BackupCoordinatorState.kt
@@ -0,0 +1,22 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.stevesoltys.seedvault.service.app.backup.coordinator
+
+import com.stevesoltys.seedvault.service.metadata.PackageState
+
+class BackupCoordinatorState(
+    var calledInitialize: Boolean,
+    var calledClearBackupData: Boolean,
+    var cancelReason: PackageState,
+) {
+    val expectFinish: Boolean
+        get() = calledInitialize || calledClearBackupData
+
+    fun onFinish() {
+        calledInitialize = false
+        calledClearBackupData = false
+        cancelReason = PackageState.UNKNOWN_ERROR
+    }
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/full/FullBackupService.kt
similarity index 85%
rename from app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/app/backup/full/FullBackupService.kt
index 48a42d64..d505986e 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/full/FullBackupService.kt
@@ -1,4 +1,8 @@
-package com.stevesoltys.seedvault.transport.backup
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.stevesoltys.seedvault.service.app.backup.full
 
 import android.app.backup.BackupTransport.FLAG_USER_INITIATED
 import android.app.backup.BackupTransport.TRANSPORT_ERROR
@@ -8,41 +12,26 @@ import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
 import android.content.pm.PackageInfo
 import android.os.ParcelFileDescriptor
 import android.util.Log
-import com.stevesoltys.seedvault.crypto.Crypto
-import com.stevesoltys.seedvault.header.VERSION
-import com.stevesoltys.seedvault.header.getADForFull
-import com.stevesoltys.seedvault.plugins.StoragePlugin
-import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.service.app.backup.InputFactory
+import com.stevesoltys.seedvault.service.crypto.CryptoService
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.header.getADForFull
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
 import libcore.io.IoUtils.closeQuietly
 import java.io.EOFException
 import java.io.IOException
-import java.io.InputStream
-import java.io.OutputStream
-
-private class FullBackupState(
-    val packageInfo: PackageInfo,
-    val inputFileDescriptor: ParcelFileDescriptor,
-    val inputStream: InputStream,
-    var outputStreamInit: (suspend () -> OutputStream)?,
-) {
-    /**
-     * This is an encrypted stream that can be written to directly.
-     */
-    var outputStream: OutputStream? = null
-    val packageName: String = packageInfo.packageName
-    var size: Long = 0
-}
 
 const val DEFAULT_QUOTA_FULL_BACKUP = (2 * (25 * 1024 * 1024)).toLong()
 
-private val TAG = FullBackup::class.java.simpleName
+private val TAG = FullBackupService::class.java.simpleName
 
 @Suppress("BlockingMethodInNonBlockingContext")
-internal class FullBackup(
+internal class FullBackupService(
     private val plugin: StoragePlugin,
-    private val settingsManager: SettingsManager,
+    private val settingsService: SettingsService,
     private val inputFactory: InputFactory,
-    private val crypto: Crypto,
+    private val cryptoService: CryptoService,
 ) {
 
     private var state: FullBackupState? = null
@@ -52,7 +41,7 @@ internal class FullBackup(
     fun getCurrentPackage() = state?.packageInfo
 
     fun getQuota(): Long {
-        return if (settingsManager.isQuotaUnlimited()) Long.MAX_VALUE else DEFAULT_QUOTA_FULL_BACKUP
+        return if (settingsService.isQuotaUnlimited()) Long.MAX_VALUE else DEFAULT_QUOTA_FULL_BACKUP
     }
 
     fun checkFullBackupSize(size: Long): Int {
@@ -114,7 +103,7 @@ internal class FullBackup(
         val inputStream = inputFactory.getInputStream(socket)
         state = FullBackupState(targetPackage, socket, inputStream) {
             Log.d(TAG, "Initializing OutputStream for $packageName.")
-            val name = crypto.getNameForPackage(salt, packageName)
+            val name = cryptoService.getNameForPackage(salt, packageName)
             // get OutputStream to write backup data into
             val outputStream = try {
                 plugin.getOutputStream(token, name)
@@ -122,12 +111,15 @@ internal class FullBackup(
                 "Error getting OutputStream for full backup of $packageName".let {
                     Log.e(TAG, it, e)
                 }
-                throw(e)
+                throw (e)
             }
             // store version header
             val state = this.state ?: throw AssertionError()
             outputStream.write(ByteArray(1) { VERSION })
-            crypto.newEncryptingStream(outputStream, getADForFull(VERSION, state.packageName))
+            cryptoService.newEncryptingStream(
+                outputStream,
+                getADForFull(VERSION, state.packageName)
+            )
         } // this lambda is only called before we actually write backup data the first time
         return TRANSPORT_OK
     }
@@ -173,7 +165,7 @@ internal class FullBackup(
 
     @Throws(IOException::class)
     suspend fun clearBackupData(packageInfo: PackageInfo, token: Long, salt: String) {
-        val name = crypto.getNameForPackage(salt, packageInfo.packageName)
+        val name = cryptoService.getNameForPackage(salt, packageInfo.packageName)
         plugin.removeData(token, name)
     }
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/full/FullBackupState.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/full/FullBackupState.kt
new file mode 100644
index 00000000..85d04450
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/full/FullBackupState.kt
@@ -0,0 +1,20 @@
+package com.stevesoltys.seedvault.service.app.backup.full
+
+import android.content.pm.PackageInfo
+import android.os.ParcelFileDescriptor
+import java.io.InputStream
+import java.io.OutputStream
+
+class FullBackupState(
+    val packageInfo: PackageInfo,
+    val inputFileDescriptor: ParcelFileDescriptor,
+    val inputStream: InputStream,
+    var outputStreamInit: (suspend () -> OutputStream)?,
+) {
+    /**
+     * This is an encrypted stream that can be written to directly.
+     */
+    var outputStream: OutputStream? = null
+    val packageName: String = packageInfo.packageName
+    var size: Long = 0
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/kv/KVBackupService.kt
similarity index 89%
rename from app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/app/backup/kv/KVBackupService.kt
index 060f5431..a3bab241 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/kv/KVBackupService.kt
@@ -1,4 +1,8 @@
-package com.stevesoltys.seedvault.transport.backup
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.stevesoltys.seedvault.service.app.backup.kv
 
 import android.app.backup.BackupTransport.FLAG_DATA_NOT_CHANGED
 import android.app.backup.BackupTransport.FLAG_INCREMENTAL
@@ -10,33 +14,25 @@ import android.content.pm.PackageInfo
 import android.os.ParcelFileDescriptor
 import android.util.Log
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
-import com.stevesoltys.seedvault.crypto.Crypto
-import com.stevesoltys.seedvault.header.VERSION
-import com.stevesoltys.seedvault.header.getADForKV
-import com.stevesoltys.seedvault.plugins.StoragePlugin
-import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.service.crypto.CryptoService
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.header.getADForKV
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.service.app.backup.InputFactory
 import java.io.IOException
 import java.util.zip.GZIPOutputStream
 
-class KVBackupState(
-    internal val packageInfo: PackageInfo,
-    val token: Long,
-    val name: String,
-    val db: KVDb,
-) {
-    var needsUpload: Boolean = false
-}
-
 const val DEFAULT_QUOTA_KEY_VALUE_BACKUP = (2 * (5 * 1024 * 1024)).toLong()
 
-private val TAG = KVBackup::class.java.simpleName
+private val TAG = KVBackupService::class.java.simpleName
 
 @Suppress("BlockingMethodInNonBlockingContext")
-internal class KVBackup(
+internal class KVBackupService(
     private val plugin: StoragePlugin,
-    private val settingsManager: SettingsManager,
+    private val settingsService: SettingsService,
     private val inputFactory: InputFactory,
-    private val crypto: Crypto,
+    private val cryptoService: CryptoService,
     private val dbManager: KvDbManager,
 ) {
 
@@ -46,7 +42,7 @@ internal class KVBackup(
 
     fun getCurrentPackage() = state?.packageInfo
 
-    fun getQuota(): Long = if (settingsManager.isQuotaUnlimited()) {
+    fun getQuota(): Long = if (settingsService.isQuotaUnlimited()) {
         Long.MAX_VALUE
     } else {
         DEFAULT_QUOTA_KEY_VALUE_BACKUP
@@ -84,7 +80,7 @@ internal class KVBackup(
         if (state != null) {
             throw AssertionError("Have state for ${state.packageInfo.packageName}")
         }
-        val name = crypto.getNameForPackage(salt, packageName)
+        val name = cryptoService.getNameForPackage(salt, packageName)
         val db = dbManager.getDb(packageName)
         this.state = KVBackupState(packageInfo, token, name, db)
 
@@ -134,7 +130,7 @@ internal class KVBackup(
                 // K/V backups (typically starting with package manager metadata - @pm@)
                 // are scheduled with JobInfo.Builder#setOverrideDeadline()
                 // and thus do not respect backoff.
-                settingsManager.canDoBackupNow()
+                settingsService.canDoBackupNow()
             } else {
                 // all other packages always need upload
                 true
@@ -194,7 +190,7 @@ internal class KVBackup(
     @Throws(IOException::class)
     suspend fun clearBackupData(packageInfo: PackageInfo, token: Long, salt: String) {
         Log.i(TAG, "Clearing K/V data of ${packageInfo.packageName}")
-        val name = state?.name ?: crypto.getNameForPackage(salt, packageInfo.packageName)
+        val name = state?.name ?: cryptoService.getNameForPackage(salt, packageInfo.packageName)
         plugin.removeData(token, name)
         if (!dbManager.deleteDb(packageInfo.packageName)) throw IOException()
     }
@@ -244,7 +240,7 @@ internal class KVBackup(
         plugin.getOutputStream(token, name).use { outputStream ->
             outputStream.write(ByteArray(1) { VERSION })
             val ad = getADForKV(VERSION, packageName)
-            crypto.newEncryptingStream(outputStream, ad).use { encryptedStream ->
+            cryptoService.newEncryptingStream(outputStream, ad).use { encryptedStream ->
                 GZIPOutputStream(encryptedStream).use { gZipStream ->
                     dbManager.getDbInputStream(packageName).use { inputStream ->
                         inputStream.copyTo(gZipStream)
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/kv/KVBackupState.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/kv/KVBackupState.kt
new file mode 100644
index 00000000..f92477dd
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/kv/KVBackupState.kt
@@ -0,0 +1,16 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.stevesoltys.seedvault.service.app.backup.kv
+
+import android.content.pm.PackageInfo
+
+class KVBackupState(
+    internal val packageInfo: PackageInfo,
+    val token: Long,
+    val name: String,
+    val db: KVDb,
+) {
+    var needsUpload: Boolean = false
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVDbManager.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/kv/KVDbManager.kt
similarity index 97%
rename from app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVDbManager.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/app/backup/kv/KVDbManager.kt
index 95a026fb..055a2536 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVDbManager.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/backup/kv/KVDbManager.kt
@@ -1,4 +1,8 @@
-package com.stevesoltys.seedvault.transport.backup
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.stevesoltys.seedvault.service.app.backup.kv
 
 import android.content.ContentValues
 import android.content.Context
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/AppRestoreService.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/AppRestoreService.kt
new file mode 100644
index 00000000..103a822e
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/AppRestoreService.kt
@@ -0,0 +1,10 @@
+package com.stevesoltys.seedvault.service.app.restore
+
+import com.stevesoltys.seedvault.ui.restore.RestorableBackup
+
+class AppRestoreService {
+
+    fun initiateRestore(restorableBackup: RestorableBackup) {
+
+    }
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/OutputFactory.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/OutputFactory.kt
similarity index 91%
rename from app/src/main/java/com/stevesoltys/seedvault/transport/restore/OutputFactory.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/app/restore/OutputFactory.kt
index 393cbfee..686b0cb1 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/OutputFactory.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/OutputFactory.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.transport.restore
+package com.stevesoltys.seedvault.service.app.restore
 
 import android.app.backup.BackupDataOutput
 import android.os.ParcelFileDescriptor
@@ -17,5 +17,4 @@ internal class OutputFactory {
     fun getOutputStream(outputFileDescriptor: ParcelFileDescriptor): OutputStream {
         return FileOutputStream(outputFileDescriptor.fileDescriptor)
     }
-
 }
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreModule.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/RestoreModule.kt
similarity index 58%
rename from app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreModule.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/app/restore/RestoreModule.kt
index 7ea9248b..b3eb820a 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreModule.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/RestoreModule.kt
@@ -1,5 +1,8 @@
-package com.stevesoltys.seedvault.transport.restore
+package com.stevesoltys.seedvault.service.app.restore
 
+import com.stevesoltys.seedvault.service.app.restore.coordinator.RestoreCoordinator
+import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
+import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
 import org.koin.android.ext.koin.androidContext
 import org.koin.dsl.module
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/coordinator/RestoreCoordinator.kt
similarity index 91%
rename from app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/app/restore/coordinator/RestoreCoordinator.kt
index 68431258..f85930c4 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/coordinator/RestoreCoordinator.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.transport.restore
+package com.stevesoltys.seedvault.service.app.restore.coordinator
 
 import android.app.backup.BackupTransport.TRANSPORT_ERROR
 import android.app.backup.BackupTransport.TRANSPORT_OK
@@ -13,15 +13,17 @@ import android.os.ParcelFileDescriptor
 import android.util.Log
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.crypto.Crypto
-import com.stevesoltys.seedvault.header.UnsupportedVersionException
-import com.stevesoltys.seedvault.metadata.BackupMetadata
-import com.stevesoltys.seedvault.metadata.BackupType
-import com.stevesoltys.seedvault.metadata.DecryptionFailedException
-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.service.app.restore.full.FullRestore
+import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
+import com.stevesoltys.seedvault.service.crypto.CryptoService
+import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
+import com.stevesoltys.seedvault.service.metadata.BackupMetadata
+import com.stevesoltys.seedvault.service.metadata.BackupType
+import com.stevesoltys.seedvault.service.metadata.DecryptionFailedException
+import com.stevesoltys.seedvault.service.metadata.MetadataReader
+import com.stevesoltys.seedvault.service.metadata.MetadataService
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
 import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
 import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
@@ -35,26 +37,14 @@ import java.io.IOException
  */
 internal const val D2D_DEVICE_NAME = "D2D"
 
-private data class RestoreCoordinatorState(
-    val token: Long,
-    val packages: Iterator<PackageInfo>,
-    /**
-     * Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@
-     */
-    val autoRestorePackageInfo: PackageInfo?,
-    val backupMetadata: BackupMetadata,
-) {
-    var currentPackage: String? = null
-}
-
 private val TAG = RestoreCoordinator::class.java.simpleName
 
 @Suppress("BlockingMethodInNonBlockingContext")
 internal class RestoreCoordinator(
     private val context: Context,
-    private val crypto: Crypto,
-    private val settingsManager: SettingsManager,
-    private val metadataManager: MetadataManager,
+    private val cryptoService: CryptoService,
+    private val settingsService: SettingsService,
+    private val metadataService: MetadataService,
     private val notificationManager: BackupNotificationManager,
     private val plugin: StoragePlugin,
     private val kv: KVRestore,
@@ -126,7 +116,7 @@ internal class RestoreCoordinator(
      * or 0 if there is no backup set available corresponding to the current device state.
      */
     fun getCurrentRestoreSet(): Long {
-        return (settingsManager.getToken() ?: 0L).apply {
+        return (settingsService.getToken() ?: 0L).apply {
             Log.i(TAG, "Got current restore set token: $this")
         }
     }
@@ -138,7 +128,7 @@ internal class RestoreCoordinator(
         this.backupMetadata = backupMetadata
 
         if (backupMetadata.d2dBackup) {
-            settingsManager.setD2dBackupsEnabled(true)
+            settingsService.setD2dBackupsEnabled(true)
         }
     }
 
@@ -167,9 +157,9 @@ internal class RestoreCoordinator(
                 // check if the backup is on removable storage that is not plugged in
                 if (isStorageRemovableAndNotAvailable()) {
                     // check if we even have a backup of that app
-                    if (metadataManager.getPackageMetadata(pmPackageName) != null) {
+                    if (metadataService.getPackageMetadata(pmPackageName) != null) {
                         // remind user to plug in storage device
-                        val storageName = settingsManager.getStorage()?.name
+                        val storageName = settingsService.getStorage()?.name
                             ?: context.getString(R.string.settings_backup_location_none)
                         notificationManager.onRemovableStorageNotAvailableForRestore(
                             pmPackageName,
@@ -231,7 +221,8 @@ internal class RestoreCoordinator(
         val type = try {
             when (state.backupMetadata.packageMetadataMap[packageName]?.backupType) {
                 BackupType.KV -> {
-                    val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
+                    val name =
+                        cryptoService.getNameForPackage(state.backupMetadata.salt, packageName)
                     if (plugin.hasData(state.token, name)) {
                         Log.i(TAG, "Found K/V data for $packageName.")
                         kv.initializeState(
@@ -247,7 +238,8 @@ internal class RestoreCoordinator(
                 }
 
                 BackupType.FULL -> {
-                    val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
+                    val name =
+                        cryptoService.getNameForPackage(state.backupMetadata.salt, packageName)
                     if (plugin.hasData(state.token, name)) {
                         Log.i(TAG, "Found full backup data for $packageName.")
                         full.initializeState(version, state.token, name, packageInfo)
@@ -365,7 +357,7 @@ internal class RestoreCoordinator(
 
     // TODO this is plugin specific, needs to be factored out when supporting different plugins
     private fun isStorageRemovableAndNotAvailable(): Boolean {
-        val storage = settingsManager.getStorage() ?: return false
+        val storage = settingsService.getStorage() ?: return false
         return storage.isUnavailableUsb(context)
     }
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/coordinator/RestoreCoordinatorState.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/coordinator/RestoreCoordinatorState.kt
new file mode 100644
index 00000000..6525000b
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/coordinator/RestoreCoordinatorState.kt
@@ -0,0 +1,20 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.stevesoltys.seedvault.service.app.restore.coordinator
+
+import android.content.pm.PackageInfo
+import com.stevesoltys.seedvault.service.metadata.BackupMetadata
+
+data class RestoreCoordinatorState(
+    val token: Long,
+    val packages: Iterator<PackageInfo>,
+    /**
+     * Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@
+     */
+    val autoRestorePackageInfo: PackageInfo?,
+    val backupMetadata: BackupMetadata,
+) {
+    var currentPackage: String? = null
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/full/FullRestore.kt
similarity index 86%
rename from app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/app/restore/full/FullRestore.kt
index 91c83e34..cb06e655 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/full/FullRestore.kt
@@ -1,4 +1,9 @@
-package com.stevesoltys.seedvault.transport.restore
+/*
+ * SPDX-FileCopyrightText: 2024 The Calyx Institute
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.stevesoltys.seedvault.service.app.restore.full
 
 import android.app.backup.BackupTransport.NO_MORE_DATA
 import android.app.backup.BackupTransport.TRANSPORT_ERROR
@@ -7,29 +12,20 @@ import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
 import android.content.pm.PackageInfo
 import android.os.ParcelFileDescriptor
 import android.util.Log
-import com.stevesoltys.seedvault.crypto.Crypto
-import com.stevesoltys.seedvault.header.HeaderReader
-import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
-import com.stevesoltys.seedvault.header.UnsupportedVersionException
-import com.stevesoltys.seedvault.header.getADForFull
-import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
-import com.stevesoltys.seedvault.plugins.StoragePlugin
+import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.app.restore.OutputFactory
+import com.stevesoltys.seedvault.service.crypto.CryptoService
+import com.stevesoltys.seedvault.service.header.HeaderDecodeService
+import com.stevesoltys.seedvault.service.header.MAX_SEGMENT_LENGTH
+import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
+import com.stevesoltys.seedvault.service.header.getADForFull
 import libcore.io.IoUtils.closeQuietly
 import java.io.EOFException
 import java.io.IOException
-import java.io.InputStream
 import java.io.OutputStream
 import java.security.GeneralSecurityException
 
-private class FullRestoreState(
-    val version: Byte,
-    val token: Long,
-    val name: String,
-    val packageInfo: PackageInfo,
-) {
-    var inputStream: InputStream? = null
-}
-
 private val TAG = FullRestore::class.java.simpleName
 
 @Suppress("BlockingMethodInNonBlockingContext")
@@ -38,8 +34,8 @@ internal class FullRestore(
     @Suppress("Deprecation")
     private val legacyPlugin: LegacyStoragePlugin,
     private val outputFactory: OutputFactory,
-    private val headerReader: HeaderReader,
-    private val crypto: Crypto,
+    private val headerDecodeService: HeaderDecodeService,
+    private val cryptoService: CryptoService,
 ) {
 
     private var state: FullRestoreState? = null
@@ -104,15 +100,15 @@ internal class FullRestore(
                 if (state.version == 0.toByte()) {
                     val inputStream =
                         legacyPlugin.getInputStreamForPackage(state.token, state.packageInfo)
-                    val version = headerReader.readVersion(inputStream, state.version)
+                    val version = headerDecodeService.readVersion(inputStream, state.version)
                     @Suppress("deprecation")
-                    crypto.decryptHeader(inputStream, version, packageName)
+                    cryptoService.decryptHeader(inputStream, version, packageName)
                     state.inputStream = inputStream
                 } else {
                     val inputStream = plugin.getInputStream(state.token, state.name)
-                    val version = headerReader.readVersion(inputStream, state.version)
+                    val version = headerDecodeService.readVersion(inputStream, state.version)
                     val ad = getADForFull(version, packageName)
-                    state.inputStream = crypto.newDecryptingStream(inputStream, ad)
+                    state.inputStream = cryptoService.newDecryptingStream(inputStream, ad)
                 }
             } catch (e: IOException) {
                 Log.w(TAG, "Error getting input stream for $packageName", e)
@@ -148,7 +144,7 @@ internal class FullRestore(
             // read segment from input stream and decrypt it
             val decrypted = try {
                 @Suppress("deprecation")
-                crypto.decryptSegment(inputStream)
+                cryptoService.decryptSegment(inputStream)
             } catch (e: EOFException) {
                 Log.i(TAG, "   EOF")
                 // close input stream here as we won't need it anymore
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/full/FullRestoreState.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/full/FullRestoreState.kt
new file mode 100644
index 00000000..89c20d67
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/full/FullRestoreState.kt
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The Calyx Institute
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.stevesoltys.seedvault.service.app.restore.full
+
+import android.content.pm.PackageInfo
+import java.io.InputStream
+
+class FullRestoreState(
+    val version: Byte,
+    val token: Long,
+    val name: String,
+    val packageInfo: PackageInfo,
+) {
+    var inputStream: InputStream? = null
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/kv/KVRestore.kt
similarity index 86%
rename from app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/app/restore/kv/KVRestore.kt
index c529c0ad..4f37393d 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/kv/KVRestore.kt
@@ -1,4 +1,9 @@
-package com.stevesoltys.seedvault.transport.restore
+/*
+ * SPDX-FileCopyrightText: 2024 The Calyx Institute
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.stevesoltys.seedvault.service.app.restore.kv
 
 import android.app.backup.BackupDataOutput
 import android.app.backup.BackupTransport.TRANSPORT_ERROR
@@ -9,34 +14,23 @@ import android.util.Log
 import com.stevesoltys.seedvault.ANCESTRAL_RECORD_KEY
 import com.stevesoltys.seedvault.GLOBAL_METADATA_KEY
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
-import com.stevesoltys.seedvault.crypto.Crypto
-import com.stevesoltys.seedvault.decodeBase64
-import com.stevesoltys.seedvault.header.HeaderReader
-import com.stevesoltys.seedvault.header.UnsupportedVersionException
-import com.stevesoltys.seedvault.header.VERSION
-import com.stevesoltys.seedvault.header.getADForKV
-import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
-import com.stevesoltys.seedvault.plugins.StoragePlugin
-import com.stevesoltys.seedvault.transport.backup.KVDb
-import com.stevesoltys.seedvault.transport.backup.KvDbManager
+import com.stevesoltys.seedvault.service.app.backup.kv.KVDb
+import com.stevesoltys.seedvault.service.app.backup.kv.KvDbManager
+import com.stevesoltys.seedvault.service.app.restore.OutputFactory
+import com.stevesoltys.seedvault.service.crypto.CryptoService
+import com.stevesoltys.seedvault.service.header.HeaderDecodeService
+import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.header.getADForKV
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
+import com.stevesoltys.seedvault.util.decodeBase64
 import libcore.io.IoUtils.closeQuietly
 import java.io.IOException
 import java.security.GeneralSecurityException
-import java.util.ArrayList
 import java.util.zip.GZIPInputStream
 import javax.crypto.AEADBadTagException
 
-private class KVRestoreState(
-    val version: Byte,
-    val token: Long,
-    val name: String,
-    val packageInfo: PackageInfo,
-    /**
-     * Optional [PackageInfo] for single package restore, optimizes restore of @pm@
-     */
-    val autoRestorePackageInfo: PackageInfo?,
-)
-
 private val TAG = KVRestore::class.java.simpleName
 
 @Suppress("BlockingMethodInNonBlockingContext")
@@ -45,8 +39,8 @@ internal class KVRestore(
     @Suppress("Deprecation")
     private val legacyPlugin: LegacyStoragePlugin,
     private val outputFactory: OutputFactory,
-    private val headerReader: HeaderReader,
-    private val crypto: Crypto,
+    private val headerDecodeService: HeaderDecodeService,
+    private val cryptoService: CryptoService,
     private val dbManager: KvDbManager,
 ) {
 
@@ -153,9 +147,9 @@ internal class KVRestore(
     private suspend fun downloadRestoreDb(state: KVRestoreState): KVDb {
         val packageName = state.packageInfo.packageName
         plugin.getInputStream(state.token, state.name).use { inputStream ->
-            headerReader.readVersion(inputStream, state.version)
+            headerDecodeService.readVersion(inputStream, state.version)
             val ad = getADForKV(VERSION, packageName)
-            crypto.newDecryptingStream(inputStream, ad).use { decryptedStream ->
+            cryptoService.newDecryptingStream(inputStream, ad).use { decryptedStream ->
                 GZIPInputStream(decryptedStream).use { gzipStream ->
                     dbManager.getDbOutputStream(packageName).use { outputStream ->
                         gzipStream.copyTo(outputStream)
@@ -241,10 +235,10 @@ internal class KVRestore(
         out: BackupDataOutput,
     ) = legacyPlugin.getInputStreamForRecord(state.token, state.packageInfo, dKey.base64Key)
         .use { inputStream ->
-            val version = headerReader.readVersion(inputStream, state.version)
+            val version = headerDecodeService.readVersion(inputStream, state.version)
             val packageName = state.packageInfo.packageName
-            crypto.decryptHeader(inputStream, version, packageName, dKey.key)
-            val value = crypto.decryptMultipleSegments(inputStream)
+            cryptoService.decryptHeader(inputStream, version, packageName, dKey.key)
+            val value = cryptoService.decryptMultipleSegments(inputStream)
             val size = value.size
             Log.v(TAG, "    ... key=${dKey.key} size=$size")
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/kv/KVRestoreState.kt b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/kv/KVRestoreState.kt
new file mode 100644
index 00000000..6372ab6f
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/app/restore/kv/KVRestoreState.kt
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The Calyx Institute
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.stevesoltys.seedvault.service.app.restore.kv
+
+import android.content.pm.PackageInfo
+
+class KVRestoreState(
+    val version: Byte,
+    val token: Long,
+    val name: String,
+    val packageInfo: PackageInfo,
+    /**
+     * Optional [PackageInfo] for single package restore, optimizes restore of @pm@
+     */
+    val autoRestorePackageInfo: PackageInfo?,
+)
diff --git a/app/src/main/java/com/stevesoltys/seedvault/crypto/CipherFactory.kt b/app/src/main/java/com/stevesoltys/seedvault/service/crypto/CipherFactory.kt
similarity index 96%
rename from app/src/main/java/com/stevesoltys/seedvault/crypto/CipherFactory.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/crypto/CipherFactory.kt
index 52d7ef88..8758e8b1 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/crypto/CipherFactory.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/crypto/CipherFactory.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.crypto
+package com.stevesoltys.seedvault.service.crypto
 
 import java.security.Key
 import javax.crypto.Cipher
diff --git a/app/src/main/java/com/stevesoltys/seedvault/crypto/CryptoModule.kt b/app/src/main/java/com/stevesoltys/seedvault/service/crypto/CryptoModule.kt
similarity index 77%
rename from app/src/main/java/com/stevesoltys/seedvault/crypto/CryptoModule.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/crypto/CryptoModule.kt
index f484bf6d..cf127f6f 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/crypto/CryptoModule.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/crypto/CryptoModule.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.crypto
+package com.stevesoltys.seedvault.service.crypto
 
 import org.koin.dsl.module
 import java.security.KeyStore
@@ -15,5 +15,5 @@ val cryptoModule = module {
         }
         KeyManagerImpl(keyStore)
     }
-    single<Crypto> { CryptoImpl(get(), get(), get()) }
+    single<CryptoService> { CryptoServiceImpl(get(), get(), get()) }
 }
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/crypto/CryptoService.kt b/app/src/main/java/com/stevesoltys/seedvault/service/crypto/CryptoService.kt
new file mode 100644
index 00000000..274b2a73
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/crypto/CryptoService.kt
@@ -0,0 +1,108 @@
+package com.stevesoltys.seedvault.service.crypto
+
+import com.google.crypto.tink.subtle.AesGcmHkdfStreaming
+import com.stevesoltys.seedvault.service.header.SegmentHeader
+import com.stevesoltys.seedvault.service.header.VersionHeader
+import java.io.EOFException
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.security.GeneralSecurityException
+import java.security.SecureRandom
+
+/**
+ * A version 1 backup stream uses [AesGcmHkdfStreaming] from the tink library.
+ *
+ * A version 0 backup stream starts with a version byte followed by an encrypted [VersionHeader].
+ *
+ * The header will be encrypted with AES/GCM to provide authentication.
+ * It can be read using [decryptHeader] which throws a [SecurityException],
+ * if the expected version and package name do not match the encrypted header.
+ *
+ * After the header, follows one or more data segments.
+ * Each segment begins with a clear-text [SegmentHeader]
+ * that contains the length of the segment
+ * and a nonce acting as the initialization vector for the encryption.
+ * The segment can be read using [decryptSegment] which throws a [SecurityException],
+ * if the length of the segment is specified larger than allowed.
+ */
+internal interface CryptoService {
+
+    /**
+     * Returns a ByteArray with bytes retrieved from [SecureRandom].
+     */
+    fun getRandomBytes(size: Int): ByteArray
+
+    fun getNameForPackage(salt: String, packageName: String): String
+
+    /**
+     * Returns the name that identifies an APK in the backup storage plugin.
+     * @param suffix empty string for normal APKs and the name of the split in case of an APK split
+     */
+    fun getNameForApk(salt: String, packageName: String, suffix: String = ""): String
+
+    /**
+     * Returns a [AesGcmHkdfStreaming] encrypting stream
+     * that gets encrypted and authenticated the given associated data.
+     */
+    @Throws(IOException::class, GeneralSecurityException::class)
+    fun newEncryptingStream(
+        outputStream: OutputStream,
+        associatedData: ByteArray,
+    ): OutputStream
+
+    /**
+     * Returns a [AesGcmHkdfStreaming] decrypting stream
+     * that gets decrypted and authenticated the given associated data.
+     */
+    @Throws(IOException::class, GeneralSecurityException::class)
+    fun newDecryptingStream(
+        inputStream: InputStream,
+        associatedData: ByteArray,
+    ): InputStream
+
+    /**
+     * Reads and decrypts a [VersionHeader] from the given [InputStream]
+     * and ensures that the expected version, package name and key match
+     * what is found in the header.
+     * If a mismatch is found, a [SecurityException] is thrown.
+     *
+     * @return The read [VersionHeader] present in the beginning of the given [InputStream].
+     */
+    @Suppress("Deprecation")
+    @Deprecated("Use newDecryptingStream instead")
+    @Throws(IOException::class, SecurityException::class)
+    fun decryptHeader(
+        inputStream: InputStream,
+        expectedVersion: Byte,
+        expectedPackageName: String,
+        expectedKey: String? = null,
+    ): VersionHeader
+
+    /**
+     * Reads and decrypts a segment from the given [InputStream].
+     *
+     * @return The decrypted segment payload.
+     */
+    @Deprecated("Use newDecryptingStream instead")
+    @Throws(EOFException::class, IOException::class, SecurityException::class)
+    fun decryptSegment(inputStream: InputStream): ByteArray
+
+    /**
+     * Like [decryptSegment], but decrypts multiple segments and does not throw [EOFException].
+     */
+    @Deprecated("Use newDecryptingStream instead")
+    @Throws(IOException::class, SecurityException::class)
+    fun decryptMultipleSegments(inputStream: InputStream): ByteArray
+
+    /**
+     * Verify that the stored backup key was created from the given seed.
+     *
+     * @return true if the key was created from given seed, false otherwise.
+     */
+    fun verifyBackupKey(seed: ByteArray): Boolean
+}
+
+internal const val TYPE_METADATA: Byte = 0x00
+internal const val TYPE_BACKUP_KV: Byte = 0x01
+internal const val TYPE_BACKUP_FULL: Byte = 0x02
diff --git a/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt b/app/src/main/java/com/stevesoltys/seedvault/service/crypto/CryptoServiceImpl.kt
similarity index 53%
rename from app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/crypto/CryptoServiceImpl.kt
index 108bcc85..2acadbf5 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/crypto/CryptoServiceImpl.kt
@@ -1,14 +1,11 @@
-package com.stevesoltys.seedvault.crypto
+package com.stevesoltys.seedvault.service.crypto
 
-import com.google.crypto.tink.subtle.AesGcmHkdfStreaming
-import com.stevesoltys.seedvault.encodeBase64
-import com.stevesoltys.seedvault.header.HeaderReader
-import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
-import com.stevesoltys.seedvault.header.MAX_VERSION_HEADER_SIZE
-import com.stevesoltys.seedvault.header.SegmentHeader
-import com.stevesoltys.seedvault.header.VersionHeader
+import com.stevesoltys.seedvault.service.header.HeaderDecodeService
+import com.stevesoltys.seedvault.service.header.MAX_SEGMENT_LENGTH
+import com.stevesoltys.seedvault.service.header.MAX_VERSION_HEADER_SIZE
+import com.stevesoltys.seedvault.service.header.VersionHeader
+import com.stevesoltys.seedvault.util.encodeBase64
 import org.calyxos.backup.storage.crypto.StreamCrypto
-import org.calyxos.backup.storage.crypto.StreamCrypto.deriveStreamKey
 import java.io.EOFException
 import java.io.IOException
 import java.io.InputStream
@@ -19,111 +16,14 @@ import java.security.NoSuchAlgorithmException
 import java.security.SecureRandom
 import javax.crypto.spec.SecretKeySpec
 
-/**
- * A version 1 backup stream uses [AesGcmHkdfStreaming] from the tink library.
- *
- * A version 0 backup stream starts with a version byte followed by an encrypted [VersionHeader].
- *
- * The header will be encrypted with AES/GCM to provide authentication.
- * It can be read using [decryptHeader] which throws a [SecurityException],
- * if the expected version and package name do not match the encrypted header.
- *
- * After the header, follows one or more data segments.
- * Each segment begins with a clear-text [SegmentHeader]
- * that contains the length of the segment
- * and a nonce acting as the initialization vector for the encryption.
- * The segment can be read using [decryptSegment] which throws a [SecurityException],
- * if the length of the segment is specified larger than allowed.
- */
-internal interface Crypto {
-
-    /**
-     * Returns a ByteArray with bytes retrieved from [SecureRandom].
-     */
-    fun getRandomBytes(size: Int): ByteArray
-
-    fun getNameForPackage(salt: String, packageName: String): String
-
-    /**
-     * Returns the name that identifies an APK in the backup storage plugin.
-     * @param suffix empty string for normal APKs and the name of the split in case of an APK split
-     */
-    fun getNameForApk(salt: String, packageName: String, suffix: String = ""): String
-
-    /**
-     * Returns a [AesGcmHkdfStreaming] encrypting stream
-     * that gets encrypted and authenticated the given associated data.
-     */
-    @Throws(IOException::class, GeneralSecurityException::class)
-    fun newEncryptingStream(
-        outputStream: OutputStream,
-        associatedData: ByteArray,
-    ): OutputStream
-
-    /**
-     * Returns a [AesGcmHkdfStreaming] decrypting stream
-     * that gets decrypted and authenticated the given associated data.
-     */
-    @Throws(IOException::class, GeneralSecurityException::class)
-    fun newDecryptingStream(
-        inputStream: InputStream,
-        associatedData: ByteArray,
-    ): InputStream
-
-    /**
-     * Reads and decrypts a [VersionHeader] from the given [InputStream]
-     * and ensures that the expected version, package name and key match
-     * what is found in the header.
-     * If a mismatch is found, a [SecurityException] is thrown.
-     *
-     * @return The read [VersionHeader] present in the beginning of the given [InputStream].
-     */
-    @Suppress("Deprecation")
-    @Deprecated("Use newDecryptingStream instead")
-    @Throws(IOException::class, SecurityException::class)
-    fun decryptHeader(
-        inputStream: InputStream,
-        expectedVersion: Byte,
-        expectedPackageName: String,
-        expectedKey: String? = null,
-    ): VersionHeader
-
-    /**
-     * Reads and decrypts a segment from the given [InputStream].
-     *
-     * @return The decrypted segment payload.
-     */
-    @Deprecated("Use newDecryptingStream instead")
-    @Throws(EOFException::class, IOException::class, SecurityException::class)
-    fun decryptSegment(inputStream: InputStream): ByteArray
-
-    /**
-     * Like [decryptSegment], but decrypts multiple segments and does not throw [EOFException].
-     */
-    @Deprecated("Use newDecryptingStream instead")
-    @Throws(IOException::class, SecurityException::class)
-    fun decryptMultipleSegments(inputStream: InputStream): ByteArray
-
-    /**
-     * Verify that the stored backup key was created from the given seed.
-     *
-     * @return true if the key was created from given seed, false otherwise.
-     */
-    fun verifyBackupKey(seed: ByteArray): Boolean
-}
-
-internal const val TYPE_METADATA: Byte = 0x00
-internal const val TYPE_BACKUP_KV: Byte = 0x01
-internal const val TYPE_BACKUP_FULL: Byte = 0x02
-
-internal class CryptoImpl(
+internal class CryptoServiceImpl(
     private val keyManager: KeyManager,
     private val cipherFactory: CipherFactory,
-    private val headerReader: HeaderReader,
-) : Crypto {
+    private val headerDecodeService: HeaderDecodeService,
+) : CryptoService {
 
     private val key: ByteArray by lazy {
-        deriveStreamKey(keyManager.getMainKey(), "app data key".toByteArray())
+        StreamCrypto.deriveStreamKey(keyManager.getMainKey(), "app data key".toByteArray())
     }
     private val secureRandom: SecureRandom by lazy { SecureRandom() }
 
@@ -175,7 +75,7 @@ internal class CryptoImpl(
         expectedKey: String?,
     ): VersionHeader {
         val decrypted = decryptSegment(inputStream, MAX_VERSION_HEADER_SIZE)
-        val header = headerReader.getVersionHeader(decrypted)
+        val header = headerDecodeService.getVersionHeader(decrypted)
 
         if (header.version != expectedVersion) {
             throw SecurityException(
@@ -219,7 +119,7 @@ internal class CryptoImpl(
     @Suppress("Deprecation")
     @Throws(EOFException::class, IOException::class, SecurityException::class)
     private fun decryptSegment(inputStream: InputStream, maxSegmentLength: Int): ByteArray {
-        val segmentHeader = headerReader.readSegmentHeader(inputStream)
+        val segmentHeader = headerDecodeService.readSegmentHeader(inputStream)
         if (segmentHeader.segmentLength > maxSegmentLength) throw SecurityException(
             "Segment length too long: ${segmentHeader.segmentLength} > $maxSegmentLength"
         )
diff --git a/app/src/main/java/com/stevesoltys/seedvault/crypto/KeyManager.kt b/app/src/main/java/com/stevesoltys/seedvault/service/crypto/KeyManager.kt
similarity index 98%
rename from app/src/main/java/com/stevesoltys/seedvault/crypto/KeyManager.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/crypto/KeyManager.kt
index 4b605fec..f41fbd5f 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/crypto/KeyManager.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/crypto/KeyManager.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.crypto
+package com.stevesoltys.seedvault.service.crypto
 
 import android.security.keystore.KeyProperties.BLOCK_MODE_GCM
 import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/file/FileBackupModule.kt b/app/src/main/java/com/stevesoltys/seedvault/service/file/FileBackupModule.kt
new file mode 100644
index 00000000..41ef219a
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/file/FileBackupModule.kt
@@ -0,0 +1,14 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.stevesoltys.seedvault.service.file
+
+import org.calyxos.backup.storage.api.StorageBackup
+import org.calyxos.backup.storage.api.StoragePlugin
+import org.koin.dsl.module
+
+val filesModule = module {
+    single<StoragePlugin> { FileBackupStoragePlugin(get(), get(), get()) }
+    single { StorageBackup(get(), get()) }
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/storage/SeedvaultStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/service/file/FileBackupStoragePlugin.kt
similarity index 76%
rename from app/src/main/java/com/stevesoltys/seedvault/storage/SeedvaultStoragePlugin.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/file/FileBackupStoragePlugin.kt
index d9657380..6ee51a1f 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/storage/SeedvaultStoragePlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/file/FileBackupStoragePlugin.kt
@@ -1,14 +1,17 @@
-package com.stevesoltys.seedvault.storage
+package com.stevesoltys.seedvault.service.file
 
 import android.content.Context
 import androidx.documentfile.provider.DocumentFile
-import com.stevesoltys.seedvault.crypto.KeyManager
 import com.stevesoltys.seedvault.getStorageContext
-import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
+import com.stevesoltys.seedvault.service.crypto.KeyManager
+import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
 import org.calyxos.backup.storage.plugin.saf.SafStoragePlugin
 import javax.crypto.SecretKey
 
-internal class SeedvaultStoragePlugin(
+/**
+ * [SafStoragePlugin] for backing up files.
+ */
+internal class FileBackupStoragePlugin(
     private val appContext: Context,
     private val storage: DocumentsStorage,
     private val keyManager: KeyManager,
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/file/backup/FileBackupJobService.kt b/app/src/main/java/com/stevesoltys/seedvault/service/file/backup/FileBackupJobService.kt
new file mode 100644
index 00000000..3f1ddac5
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/file/backup/FileBackupJobService.kt
@@ -0,0 +1,19 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.stevesoltys.seedvault.service.file.backup
+
+import org.calyxos.backup.storage.backup.BackupJobService
+
+/*
+test and debug with
+
+  adb shell dumpsys jobscheduler |
+  grep -A 23 -B 4 "Service: com.stevesoltys.seedvault/.storage.StorageBackupJobService"
+
+force running with:
+
+  adb shell cmd jobscheduler run -f com.stevesoltys.seedvault 0
+
+ */
+internal class FileBackupJobService : BackupJobService(FileBackupService::class.java)
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/file/backup/FileBackupService.kt b/app/src/main/java/com/stevesoltys/seedvault/service/file/backup/FileBackupService.kt
new file mode 100644
index 00000000..4993cc2c
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/file/backup/FileBackupService.kt
@@ -0,0 +1,32 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.stevesoltys.seedvault.service.file.backup
+
+import android.content.Intent
+import com.stevesoltys.seedvault.service.app.backup.requestBackup
+import org.calyxos.backup.storage.api.BackupObserver
+import org.calyxos.backup.storage.api.StorageBackup
+import org.calyxos.backup.storage.backup.BackupService
+import org.calyxos.backup.storage.backup.NotificationBackupObserver
+import org.koin.android.ext.android.inject
+
+internal class FileBackupService : BackupService() {
+
+    companion object {
+        internal const val EXTRA_START_APP_BACKUP = "startAppBackup"
+    }
+
+    override val storageBackup: StorageBackup by inject()
+
+    // use lazy delegate because context isn't available during construction time
+    override val backupObserver: BackupObserver by lazy {
+        NotificationBackupObserver(applicationContext)
+    }
+
+    override fun onBackupFinished(intent: Intent, success: Boolean) {
+        if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) {
+            requestBackup(applicationContext)
+        }
+    }
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/file/restore/FileRestoreService.kt b/app/src/main/java/com/stevesoltys/seedvault/service/file/restore/FileRestoreService.kt
new file mode 100644
index 00000000..2e8cec8c
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/file/restore/FileRestoreService.kt
@@ -0,0 +1,19 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.stevesoltys.seedvault.service.file.restore
+
+import org.calyxos.backup.storage.api.RestoreObserver
+import org.calyxos.backup.storage.api.StorageBackup
+import org.calyxos.backup.storage.restore.NotificationRestoreObserver
+import org.calyxos.backup.storage.restore.RestoreService
+import org.koin.android.ext.android.inject
+
+internal class FileRestoreService : RestoreService() {
+    override val storageBackup: StorageBackup by inject()
+
+    // use lazy delegate because context isn't available during construction time
+    override val restoreObserver: RestoreObserver by lazy {
+        NotificationRestoreObserver(applicationContext)
+    }
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/header/Header.kt b/app/src/main/java/com/stevesoltys/seedvault/service/header/Header.kt
similarity index 90%
rename from app/src/main/java/com/stevesoltys/seedvault/header/Header.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/header/Header.kt
index 17be0016..150e9710 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/header/Header.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/header/Header.kt
@@ -1,8 +1,8 @@
-package com.stevesoltys.seedvault.header
+package com.stevesoltys.seedvault.service.header
 
-import com.stevesoltys.seedvault.crypto.GCM_AUTHENTICATION_TAG_LENGTH
-import com.stevesoltys.seedvault.crypto.TYPE_BACKUP_FULL
-import com.stevesoltys.seedvault.crypto.TYPE_BACKUP_KV
+import com.stevesoltys.seedvault.service.crypto.GCM_AUTHENTICATION_TAG_LENGTH
+import com.stevesoltys.seedvault.service.crypto.TYPE_BACKUP_FULL
+import com.stevesoltys.seedvault.service.crypto.TYPE_BACKUP_KV
 import java.nio.ByteBuffer
 
 internal const val VERSION: Byte = 1
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/header/HeaderDecodeService.kt b/app/src/main/java/com/stevesoltys/seedvault/service/header/HeaderDecodeService.kt
new file mode 100644
index 00000000..c7bd5b7e
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/header/HeaderDecodeService.kt
@@ -0,0 +1,22 @@
+package com.stevesoltys.seedvault.service.header
+
+import java.io.EOFException
+import java.io.IOException
+import java.io.InputStream
+
+internal interface HeaderDecodeService {
+    @Throws(IOException::class, UnsupportedVersionException::class)
+    fun readVersion(inputStream: InputStream, expectedVersion: Byte): Byte
+
+    @Suppress("Deprecation")
+    @Deprecated("For restoring v0 backups only")
+    @Throws(SecurityException::class)
+    fun getVersionHeader(byteArray: ByteArray): VersionHeader
+
+    @Suppress("Deprecation")
+    @Deprecated("For restoring v0 backups only")
+    @Throws(EOFException::class, IOException::class)
+    fun readSegmentHeader(inputStream: InputStream): SegmentHeader
+}
+
+class UnsupportedVersionException(val version: Byte) : IOException()
diff --git a/app/src/main/java/com/stevesoltys/seedvault/header/HeaderReader.kt b/app/src/main/java/com/stevesoltys/seedvault/service/header/HeaderDecodeServiceImpl.kt
similarity index 79%
rename from app/src/main/java/com/stevesoltys/seedvault/header/HeaderReader.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/header/HeaderDecodeServiceImpl.kt
index 1f6c77fb..29ce253a 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/header/HeaderReader.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/header/HeaderDecodeServiceImpl.kt
@@ -1,28 +1,13 @@
-package com.stevesoltys.seedvault.header
+package com.stevesoltys.seedvault.service.header
 
-import com.stevesoltys.seedvault.Utf8
+import com.stevesoltys.seedvault.util.Utf8
 import java.io.EOFException
 import java.io.IOException
 import java.io.InputStream
 import java.nio.ByteBuffer
 import java.security.GeneralSecurityException
 
-internal interface HeaderReader {
-    @Throws(IOException::class, UnsupportedVersionException::class)
-    fun readVersion(inputStream: InputStream, expectedVersion: Byte): Byte
-
-    @Suppress("Deprecation")
-    @Deprecated("For restoring v0 backups only")
-    @Throws(SecurityException::class)
-    fun getVersionHeader(byteArray: ByteArray): VersionHeader
-
-    @Suppress("Deprecation")
-    @Deprecated("For restoring v0 backups only")
-    @Throws(EOFException::class, IOException::class)
-    fun readSegmentHeader(inputStream: InputStream): SegmentHeader
-}
-
-internal class HeaderReaderImpl : HeaderReader {
+internal class HeaderDecodeServiceImpl : HeaderDecodeService {
 
     @Throws(IOException::class, UnsupportedVersionException::class, GeneralSecurityException::class)
     override fun readVersion(inputStream: InputStream, expectedVersion: Byte): Byte {
@@ -85,5 +70,3 @@ internal class HeaderReaderImpl : HeaderReader {
     }
 
 }
-
-class UnsupportedVersionException(val version: Byte) : IOException()
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/header/HeaderModule.kt b/app/src/main/java/com/stevesoltys/seedvault/service/header/HeaderModule.kt
new file mode 100644
index 00000000..8b38a1ec
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/header/HeaderModule.kt
@@ -0,0 +1,7 @@
+package com.stevesoltys.seedvault.service.header
+
+import org.koin.dsl.module
+
+val headerModule = module {
+    single<HeaderDecodeService> { HeaderDecodeServiceImpl() }
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt b/app/src/main/java/com/stevesoltys/seedvault/service/metadata/Metadata.kt
similarity index 93%
rename from app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/metadata/Metadata.kt
index c8ad6aac..b2d56f70 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/metadata/Metadata.kt
@@ -1,10 +1,10 @@
-package com.stevesoltys.seedvault.metadata
+package com.stevesoltys.seedvault.service.metadata
 
 import android.content.pm.ApplicationInfo.FLAG_STOPPED
 import android.os.Build
-import com.stevesoltys.seedvault.crypto.TYPE_METADATA
-import com.stevesoltys.seedvault.header.VERSION
-import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
+import com.stevesoltys.seedvault.service.crypto.TYPE_METADATA
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
 import org.calyxos.backup.storage.crypto.StreamCrypto.toByteArray
 import java.nio.ByteBuffer
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt b/app/src/main/java/com/stevesoltys/seedvault/service/metadata/MetadataModule.kt
similarity index 67%
rename from app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/metadata/MetadataModule.kt
index 0d7ed1a2..8d231f89 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/metadata/MetadataModule.kt
@@ -1,10 +1,10 @@
-package com.stevesoltys.seedvault.metadata
+package com.stevesoltys.seedvault.service.metadata
 
 import org.koin.android.ext.koin.androidContext
 import org.koin.dsl.module
 
 val metadataModule = module {
-    single { MetadataManager(androidContext(), get(), get(), get(), get(), get()) }
+    single { MetadataService(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/service/metadata/MetadataReader.kt
similarity index 86%
rename from app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/metadata/MetadataReader.kt
index bbd6df19..eb5f046b 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/metadata/MetadataReader.kt
@@ -1,15 +1,15 @@
-package com.stevesoltys.seedvault.metadata
+package com.stevesoltys.seedvault.service.metadata
 
-import com.stevesoltys.seedvault.Utf8
-import com.stevesoltys.seedvault.crypto.Crypto
-import com.stevesoltys.seedvault.header.UnsupportedVersionException
-import com.stevesoltys.seedvault.header.VERSION
-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.QUOTA_EXCEEDED
-import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
-import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
+import com.stevesoltys.seedvault.service.crypto.CryptoService
+import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
+import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.QUOTA_EXCEEDED
+import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
+import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
+import com.stevesoltys.seedvault.util.Utf8
 import org.json.JSONException
 import org.json.JSONObject
 import java.io.IOException
@@ -36,7 +36,7 @@ interface MetadataReader {
 
 }
 
-internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
+internal class MetadataReaderImpl(private val cryptoService: CryptoService) : MetadataReader {
 
     @Throws(
         SecurityException::class,
@@ -51,7 +51,8 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
         if (version == 0.toByte()) return readMetadataV0(inputStream, expectedToken)
 
         val metadataBytes = try {
-            crypto.newDecryptingStream(inputStream, getAD(version, expectedToken)).readBytes()
+            cryptoService.newDecryptingStream(inputStream, getAD(version, expectedToken))
+                .readBytes()
         } catch (e: GeneralSecurityException) {
             throw DecryptionFailedException(e)
         }
@@ -67,7 +68,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
     @Suppress("Deprecation")
     private fun readMetadataV0(inputStream: InputStream, expectedToken: Long): BackupMetadata {
         val metadataBytes = try {
-            crypto.decryptMultipleSegments(inputStream)
+            cryptoService.decryptMultipleSegments(inputStream)
         } catch (e: AEADBadTagException) {
             throw DecryptionFailedException(e)
         }
diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt b/app/src/main/java/com/stevesoltys/seedvault/service/metadata/MetadataService.kt
similarity index 89%
rename from app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/metadata/MetadataService.kt
index f58a29a1..d9178221 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/metadata/MetadataService.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.metadata
+package com.stevesoltys.seedvault.service.metadata
 
 import android.content.Context
 import android.content.Context.MODE_PRIVATE
@@ -9,34 +9,34 @@ import androidx.annotation.WorkerThread
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.distinctUntilChanged
-import com.stevesoltys.seedvault.Clock
-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.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 com.stevesoltys.seedvault.service.app.isSystemApp
+import com.stevesoltys.seedvault.service.crypto.CryptoService
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
+import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.util.TimeSource
+import com.stevesoltys.seedvault.util.encodeBase64
 import java.io.FileNotFoundException
 import java.io.IOException
 import java.io.OutputStream
 
-private val TAG = MetadataManager::class.java.simpleName
+private val TAG = MetadataService::class.java.simpleName
 
 @VisibleForTesting
 internal const val METADATA_CACHE_FILE = "metadata.cache"
 internal const val METADATA_SALT_SIZE = 32
 
 @WorkerThread
-internal class MetadataManager(
+internal class MetadataService(
     private val context: Context,
-    private val clock: Clock,
-    private val crypto: Crypto,
+    private val timeSource: TimeSource,
+    private val cryptoService: CryptoService,
     private val metadataWriter: MetadataWriter,
     private val metadataReader: MetadataReader,
-    private val settingsManager: SettingsManager
+    private val settingsService: SettingsService
 ) {
 
     private val uninitializedMetadata = BackupMetadata(token = 0L, salt = "")
@@ -63,7 +63,7 @@ internal class MetadataManager(
     @Synchronized
     @Throws(IOException::class)
     fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) {
-        val salt = crypto.getRandomBytes(METADATA_SALT_SIZE).encodeBase64()
+        val salt = cryptoService.getRandomBytes(METADATA_SALT_SIZE).encodeBase64()
         modifyMetadata(metadataOutputStream) {
             metadata = BackupMetadata(token = token, salt = salt)
         }
@@ -135,9 +135,9 @@ internal class MetadataManager(
     ) {
         val packageName = packageInfo.packageName
         modifyMetadata(metadataOutputStream) {
-            val now = clock.time()
+            val now = timeSource.time()
             metadata.time = now
-            metadata.d2dBackup = settingsManager.d2dBackupsEnabled()
+            metadata.d2dBackup = settingsService.d2dBackupsEnabled()
 
             if (metadata.packageMetadataMap.containsKey(packageName)) {
                 metadata.packageMetadataMap[packageName]!!.time = now
diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt b/app/src/main/java/com/stevesoltys/seedvault/service/metadata/MetadataWriter.kt
similarity index 84%
rename from app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/metadata/MetadataWriter.kt
index bbed50c7..44839ffb 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/metadata/MetadataWriter.kt
@@ -1,8 +1,8 @@
-package com.stevesoltys.seedvault.metadata
+package com.stevesoltys.seedvault.service.metadata
 
-import com.stevesoltys.seedvault.Utf8
-import com.stevesoltys.seedvault.crypto.Crypto
-import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
+import com.stevesoltys.seedvault.service.crypto.CryptoService
+import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
+import com.stevesoltys.seedvault.util.Utf8
 import org.json.JSONArray
 import org.json.JSONObject
 import java.io.IOException
@@ -15,14 +15,15 @@ interface MetadataWriter {
     fun encode(metadata: BackupMetadata): ByteArray
 }
 
-internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
+internal class MetadataWriterImpl(private val cryptoService: CryptoService) : MetadataWriter {
 
     @Throws(IOException::class)
     override fun write(metadata: BackupMetadata, outputStream: OutputStream) {
         outputStream.write(ByteArray(1).apply { this[0] = metadata.version })
-        crypto.newEncryptingStream(outputStream, getAD(metadata.version, metadata.token)).use {
-            it.write(encode(metadata))
-        }
+        cryptoService.newEncryptingStream(outputStream, getAD(metadata.version, metadata.token))
+            .use {
+                it.write(encode(metadata))
+            }
     }
 
     override fun encode(metadata: BackupMetadata): ByteArray {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/settings/FlashDrive.kt b/app/src/main/java/com/stevesoltys/seedvault/service/settings/FlashDrive.kt
new file mode 100644
index 00000000..b64cabdc
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/settings/FlashDrive.kt
@@ -0,0 +1,19 @@
+package com.stevesoltys.seedvault.service.settings
+
+import android.hardware.usb.UsbDevice
+
+data class FlashDrive(
+    val name: String,
+    val serialNumber: String?,
+    val vendorId: Int,
+    val productId: Int,
+) {
+    companion object {
+        fun from(device: UsbDevice) = FlashDrive(
+            name = "${device.manufacturerName} ${device.productName}",
+            serialNumber = device.serialNumber,
+            vendorId = device.vendorId,
+            productId = device.productId
+        )
+    }
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/service/settings/SettingsService.kt
similarity index 74%
rename from app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/settings/SettingsService.kt
index 2647e57b..eba1ccca 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/settings/SettingsService.kt
@@ -1,17 +1,14 @@
-package com.stevesoltys.seedvault.settings
+package com.stevesoltys.seedvault.service.settings
 
 import android.content.Context
-import android.hardware.usb.UsbDevice
-import android.net.ConnectivityManager
-import android.net.NetworkCapabilities
 import android.net.Uri
 import androidx.annotation.UiThread
 import androidx.annotation.WorkerThread
-import androidx.documentfile.provider.DocumentFile
 import androidx.preference.PreferenceManager
 import com.stevesoltys.seedvault.getStorageContext
 import com.stevesoltys.seedvault.permitDiskReads
-import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
+import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
+import com.stevesoltys.seedvault.ui.settings.AppStatus
 import java.util.concurrent.ConcurrentSkipListSet
 
 internal const val PREF_KEY_TOKEN = "token"
@@ -34,7 +31,9 @@ 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) {
+class SettingsService(
+    private val context: Context,
+) {
 
     private val prefs = permitDiskReads {
         PreferenceManager.getDefaultSharedPreferences(context)
@@ -59,7 +58,7 @@ class SettingsManager(private val context: Context) {
 
     /**
      * Sets a new RestoreSet token.
-     * Should only be called by the [BackupCoordinator]
+     * Should only be called by the [BackupCoordinatorService]
      * to ensure that related work is performed after moving to a new token.
      */
     fun setNewToken(newToken: Long?) {
@@ -162,54 +161,3 @@ class SettingsManager(private val context: Context) {
             .apply()
     }
 }
-
-data class Storage(
-    val uri: Uri,
-    val name: String,
-    val isUsb: Boolean,
-    val requiresNetwork: Boolean,
-) {
-    fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, uri)
-        ?: throw AssertionError("Should only happen on API < 21.")
-
-    /**
-     * Returns true if this is USB storage that is not available, false otherwise.
-     *
-     * Must be run off UI thread (ideally I/O).
-     */
-    @WorkerThread
-    fun isUnavailableUsb(context: Context): Boolean {
-        return isUsb && !getDocumentFile(context).isDirectory
-    }
-
-    /**
-     * 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)
-    }
-
-    private fun hasUnmeteredInternet(context: Context): Boolean {
-        val cm = context.getSystemService(ConnectivityManager::class.java)
-        val isMetered = cm.isActiveNetworkMetered
-        val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
-        return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && !isMetered
-    }
-}
-
-data class FlashDrive(
-    val name: String,
-    val serialNumber: String?,
-    val vendorId: Int,
-    val productId: Int,
-) {
-    companion object {
-        fun from(device: UsbDevice) = FlashDrive(
-            name = "${device.manufacturerName} ${device.productName}",
-            serialNumber = device.serialNumber,
-            vendorId = device.vendorId,
-            productId = device.productId
-        )
-    }
-}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/settings/Storage.kt b/app/src/main/java/com/stevesoltys/seedvault/service/settings/Storage.kt
new file mode 100644
index 00000000..c8d9957f
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/settings/Storage.kt
@@ -0,0 +1,43 @@
+package com.stevesoltys.seedvault.service.settings
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
+import android.net.Uri
+import androidx.annotation.WorkerThread
+import androidx.documentfile.provider.DocumentFile
+
+data class Storage(
+    val uri: Uri,
+    val name: String,
+    val isUsb: Boolean,
+    val requiresNetwork: Boolean,
+) {
+    fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, uri)
+        ?: throw AssertionError("Should only happen on API < 21.")
+
+    /**
+     * Returns true if this is USB storage that is not available, false otherwise.
+     *
+     * Must be run off UI thread (ideally I/O).
+     */
+    @WorkerThread
+    fun isUnavailableUsb(context: Context): Boolean {
+        return isUsb && !getDocumentFile(context).isDirectory
+    }
+
+    /**
+     * 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)
+    }
+
+    private fun hasUnmeteredInternet(context: Context): Boolean {
+        val cm = context.getSystemService(ConnectivityManager::class.java)
+        val isMetered = cm.isActiveNetworkMetered
+        val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
+        return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && !isMetered
+    }
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/service/storage/EncryptedBackupMetadata.kt b/app/src/main/java/com/stevesoltys/seedvault/service/storage/EncryptedBackupMetadata.kt
new file mode 100644
index 00000000..44980e57
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/storage/EncryptedBackupMetadata.kt
@@ -0,0 +1,8 @@
+package com.stevesoltys.seedvault.service.storage
+
+import java.io.InputStream
+
+class EncryptedBackupMetadata(
+    val token: Long,
+    val inputStreamRetriever: () -> InputStream,
+)
diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/service/storage/StoragePlugin.kt
similarity index 89%
rename from app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/storage/StoragePlugin.kt
index 53becfac..bd530628 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/storage/StoragePlugin.kt
@@ -1,8 +1,8 @@
-package com.stevesoltys.seedvault.plugins
+package com.stevesoltys.seedvault.service.storage
 
 import android.app.backup.RestoreSet
 import androidx.annotation.WorkerThread
-import com.stevesoltys.seedvault.settings.Storage
+import com.stevesoltys.seedvault.service.settings.Storage
 import java.io.IOException
 import java.io.InputStream
 import java.io.OutputStream
@@ -61,7 +61,7 @@ interface StoragePlugin {
      * @return metadata for the set of restore images available,
      * or null if an error occurred (the attempt should be rescheduled).
      **/
-    suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>?
+    suspend fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>?
 
     /**
      * Returns the package name of the app that provides the backend storage
@@ -74,5 +74,3 @@ interface StoragePlugin {
     val providerPackageName: String?
 
 }
-
-class EncryptedMetadata(val token: Long, val inputStreamRetriever: () -> InputStream)
diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt b/app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/DocumentsProviderModule.kt
similarity index 57%
rename from app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/DocumentsProviderModule.kt
index 638ee990..51da8dd4 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/DocumentsProviderModule.kt
@@ -1,7 +1,8 @@
-package com.stevesoltys.seedvault.plugins.saf
+package com.stevesoltys.seedvault.service.storage.saf
 
-import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
-import com.stevesoltys.seedvault.plugins.StoragePlugin
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.storage.saf.legacy.DocumentsProviderLegacyPlugin
+import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
 import org.koin.android.ext.koin.androidContext
 import org.koin.dsl.module
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/DocumentsProviderStoragePlugin.kt
similarity index 94%
rename from app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/DocumentsProviderStoragePlugin.kt
index 0dbc6c50..8069f9b6 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/DocumentsProviderStoragePlugin.kt
@@ -1,13 +1,13 @@
-package com.stevesoltys.seedvault.plugins.saf
+package com.stevesoltys.seedvault.service.storage.saf
 
 import android.content.Context
 import android.content.pm.PackageManager
 import android.util.Log
 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.settings.Storage
+import com.stevesoltys.seedvault.service.storage.EncryptedBackupMetadata
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.settings.Storage
 import java.io.FileNotFoundException
 import java.io.IOException
 import java.io.InputStream
@@ -90,14 +90,14 @@ internal class DocumentsProviderStoragePlugin(
         return backupSets.isNotEmpty()
     }
 
-    override suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>? {
+    override suspend fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? {
         val rootDir = storage.rootBackupDir ?: return null
         val backupSets = getBackups(context, rootDir)
         val iterator = backupSets.iterator()
         return generateSequence {
             if (!iterator.hasNext()) return@generateSequence null // end sequence
             val backupSet = iterator.next()
-            EncryptedMetadata(backupSet.token) {
+            EncryptedBackupMetadata(backupSet.token) {
                 storage.getInputStream(backupSet.metadataFile)
             }
         }
diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt b/app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/DocumentsStorage.kt
similarity index 96%
rename from app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/DocumentsStorage.kt
index 7e970a73..b9ca50fb 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/DocumentsStorage.kt
@@ -1,6 +1,6 @@
 @file:Suppress("BlockingMethodInNonBlockingContext")
 
-package com.stevesoltys.seedvault.plugins.saf
+package com.stevesoltys.seedvault.service.storage.saf
 
 import android.content.ContentResolver
 import android.content.Context
@@ -17,8 +17,8 @@ import android.util.Log
 import androidx.annotation.VisibleForTesting
 import androidx.documentfile.provider.DocumentFile
 import com.stevesoltys.seedvault.getStorageContext
-import com.stevesoltys.seedvault.settings.SettingsManager
-import com.stevesoltys.seedvault.settings.Storage
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.service.settings.Storage
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.suspendCancellableCoroutine
@@ -43,11 +43,11 @@ private val TAG = DocumentsStorage::class.java.simpleName
 
 internal class DocumentsStorage(
     private val appContext: Context,
-    private val settingsManager: SettingsManager,
+    private val settingsService: SettingsService,
 ) {
     internal var storage: Storage? = null
         get() {
-            if (field == null) field = settingsManager.getStorage()
+            if (field == null) field = settingsService.getStorage()
             return field
         }
 
@@ -81,7 +81,7 @@ internal class DocumentsStorage(
 
     private var currentToken: Long? = null
         get() {
-            if (field == null) field = settingsManager.getToken()
+            if (field == null) field = settingsService.getToken()
             return field
         }
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderLegacyPlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/legacy/DocumentsProviderLegacyPlugin.kt
similarity index 90%
rename from app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderLegacyPlugin.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/legacy/DocumentsProviderLegacyPlugin.kt
index 1a1d075f..d4300591 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderLegacyPlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/legacy/DocumentsProviderLegacyPlugin.kt
@@ -1,16 +1,20 @@
-package com.stevesoltys.seedvault.plugins.saf
+package com.stevesoltys.seedvault.service.storage.saf.legacy
 
 import android.content.Context
 import android.content.pm.PackageInfo
 import androidx.annotation.WorkerThread
 import androidx.documentfile.provider.DocumentFile
-import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
+import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
+import com.stevesoltys.seedvault.service.storage.saf.assertRightFile
+import com.stevesoltys.seedvault.service.storage.saf.findFileBlocking
+import com.stevesoltys.seedvault.service.storage.saf.listFilesBlocking
 import java.io.FileNotFoundException
 import java.io.IOException
 import java.io.InputStream
 
 @WorkerThread
 @Suppress("BlockingMethodInNonBlockingContext", "Deprecation") // all methods do I/O
+@Deprecated("Only for old v0 backup format")
 internal class DocumentsProviderLegacyPlugin(
     private val context: Context,
     private val storage: DocumentsStorage,
diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/LegacyStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/legacy/LegacyStoragePlugin.kt
similarity index 96%
rename from app/src/main/java/com/stevesoltys/seedvault/plugins/LegacyStoragePlugin.kt
rename to app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/legacy/LegacyStoragePlugin.kt
index 8082819b..f5f3fb7e 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/plugins/LegacyStoragePlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/service/storage/saf/legacy/LegacyStoragePlugin.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.plugins
+package com.stevesoltys.seedvault.service.storage.saf.legacy
 
 import android.content.pm.PackageInfo
 import java.io.IOException
diff --git a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt b/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt
deleted file mode 100644
index 1c54beb2..00000000
--- a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.stevesoltys.seedvault.storage
-
-import android.content.Intent
-import com.stevesoltys.seedvault.transport.requestBackup
-import org.calyxos.backup.storage.api.BackupObserver
-import org.calyxos.backup.storage.api.RestoreObserver
-import org.calyxos.backup.storage.api.StorageBackup
-import org.calyxos.backup.storage.backup.BackupJobService
-import org.calyxos.backup.storage.backup.BackupService
-import org.calyxos.backup.storage.backup.NotificationBackupObserver
-import org.calyxos.backup.storage.restore.NotificationRestoreObserver
-import org.calyxos.backup.storage.restore.RestoreService
-import org.koin.android.ext.android.inject
-
-/*
-test and debug with
-
-  adb shell dumpsys jobscheduler |
-  grep -A 23 -B 4 "Service: com.stevesoltys.seedvault/.storage.StorageBackupJobService"
-
-force running with:
-
-  adb shell cmd jobscheduler run -f com.stevesoltys.seedvault 0
-
- */
-internal class StorageBackupJobService : BackupJobService(StorageBackupService::class.java)
-
-internal class StorageBackupService : BackupService() {
-
-    companion object {
-        internal const val EXTRA_START_APP_BACKUP = "startAppBackup"
-    }
-
-    override val storageBackup: StorageBackup by inject()
-
-    // use lazy delegate because context isn't available during construction time
-    override val backupObserver: BackupObserver by lazy {
-        NotificationBackupObserver(applicationContext)
-    }
-
-    override fun onBackupFinished(intent: Intent, success: Boolean) {
-        if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) {
-            requestBackup(applicationContext)
-        }
-    }
-}
-
-internal class StorageRestoreService : RestoreService() {
-    override val storageBackup: StorageBackup by inject()
-
-    // use lazy delegate because context isn't available during construction time
-    override val restoreObserver: RestoreObserver by lazy {
-        NotificationRestoreObserver(applicationContext)
-    }
-}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/storage/StorageModule.kt b/app/src/main/java/com/stevesoltys/seedvault/storage/StorageModule.kt
deleted file mode 100644
index 68254e88..00000000
--- a/app/src/main/java/com/stevesoltys/seedvault/storage/StorageModule.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.stevesoltys.seedvault.storage
-
-import org.calyxos.backup.storage.api.StorageBackup
-import org.calyxos.backup.storage.api.StoragePlugin
-import org.koin.dsl.module
-
-val storageModule = module {
-    single<StoragePlugin> { SeedvaultStoragePlugin(get(), get(), get()) }
-    single { StorageBackup(get(), get()) }
-}
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 cead1eab..04423495 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt
@@ -11,10 +11,10 @@ import android.content.pm.PackageInfo
 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 com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
+import com.stevesoltys.seedvault.service.app.restore.coordinator.RestoreCoordinator
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.ui.settings.SettingsActivity
 import kotlinx.coroutines.runBlocking
 import org.koin.core.component.KoinComponent
 import org.koin.core.component.inject
@@ -36,9 +36,9 @@ private val TAG = ConfigurableBackupTransport::class.java.simpleName
 class ConfigurableBackupTransport internal constructor(private val context: Context) :
     BackupTransport(), KoinComponent {
 
-    private val backupCoordinator by inject<BackupCoordinator>()
+    private val backupCoordinatorService by inject<BackupCoordinatorService>()
     private val restoreCoordinator by inject<RestoreCoordinator>()
-    private val settingsManager by inject<SettingsManager>()
+    private val settingsService by inject<SettingsService>()
 
     override fun transportDirName(): String {
         return TRANSPORT_DIRECTORY_NAME
@@ -58,7 +58,7 @@ 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 if (settingsManager.d2dBackupsEnabled()) {
+        return if (settingsService.d2dBackupsEnabled()) {
             D2D_TRANSPORT_FLAGS
         } else {
             DEFAULT_TRANSPORT_FLAGS
@@ -120,26 +120,26 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
     //
 
     override fun initializeDevice(): Int = runBlocking {
-        backupCoordinator.initializeDevice()
+        backupCoordinatorService.initializeDevice()
     }
 
     override fun isAppEligibleForBackup(
         targetPackage: PackageInfo,
         isFullBackup: Boolean,
     ): Boolean {
-        return backupCoordinator.isAppEligibleForBackup(targetPackage, isFullBackup)
+        return backupCoordinatorService.isAppEligibleForBackup(targetPackage, isFullBackup)
     }
 
     override fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long = runBlocking {
-        backupCoordinator.getBackupQuota(packageName, isFullBackup)
+        backupCoordinatorService.getBackupQuota(packageName, isFullBackup)
     }
 
     override fun clearBackupData(packageInfo: PackageInfo): Int = runBlocking {
-        backupCoordinator.clearBackupData(packageInfo)
+        backupCoordinatorService.clearBackupData(packageInfo)
     }
 
     override fun finishBackup(): Int = runBlocking {
-        backupCoordinator.finishBackup()
+        backupCoordinatorService.finishBackup()
     }
 
     // ------------------------------------------------------------------------------------
@@ -147,7 +147,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
     //
 
     override fun requestBackupTime(): Long {
-        return backupCoordinator.requestBackupTime()
+        return backupCoordinatorService.requestBackupTime()
     }
 
     override fun performBackup(
@@ -155,7 +155,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
         inFd: ParcelFileDescriptor,
         flags: Int,
     ): Int = runBlocking {
-        backupCoordinator.performIncrementalBackup(packageInfo, inFd, flags)
+        backupCoordinatorService.performIncrementalBackup(packageInfo, inFd, flags)
     }
 
     override fun performBackup(
@@ -171,11 +171,11 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
     //
 
     override fun requestFullBackupTime(): Long {
-        return backupCoordinator.requestFullBackupTime()
+        return backupCoordinatorService.requestFullBackupTime()
     }
 
     override fun checkFullBackupSize(size: Long): Int {
-        return backupCoordinator.checkFullBackupSize(size)
+        return backupCoordinatorService.checkFullBackupSize(size)
     }
 
     override fun performFullBackup(
@@ -183,7 +183,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
         socket: ParcelFileDescriptor,
         flags: Int,
     ): Int = runBlocking {
-        backupCoordinator.performFullBackup(targetPackage, socket, flags)
+        backupCoordinatorService.performFullBackup(targetPackage, socket, flags)
     }
 
     override fun performFullBackup(
@@ -191,15 +191,15 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
         fileDescriptor: ParcelFileDescriptor,
     ): Int = runBlocking {
         Log.w(TAG, "Warning: Legacy performFullBackup() method called.")
-        backupCoordinator.performFullBackup(targetPackage, fileDescriptor, 0)
+        backupCoordinatorService.performFullBackup(targetPackage, fileDescriptor, 0)
     }
 
     override fun sendBackupData(numBytes: Int): Int = runBlocking {
-        backupCoordinator.sendBackupData(numBytes)
+        backupCoordinatorService.sendBackupData(numBytes)
     }
 
     override fun cancelFullBackup() = runBlocking {
-        backupCoordinator.cancelFullBackup()
+        backupCoordinatorService.cancelFullBackup()
     }
 
     // ------------------------------------------------------------------------------------
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 1d67988d..900df27f 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt
@@ -1,22 +1,14 @@
 package com.stevesoltys.seedvault.transport
 
 import android.app.Service
-import android.app.backup.BackupManager
 import android.app.backup.IBackupManager
-import android.content.Context
 import android.content.Intent
 import android.os.IBinder
-import android.os.RemoteException
 import android.util.Log
-import androidx.annotation.WorkerThread
-import com.stevesoltys.seedvault.BackupMonitor
-import com.stevesoltys.seedvault.crypto.KeyManager
-import com.stevesoltys.seedvault.transport.backup.PackageService
+import com.stevesoltys.seedvault.service.crypto.KeyManager
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
-import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver
 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
 
@@ -59,30 +51,3 @@ class ConfigurableBackupTransportService : Service(), KoinComponent {
     }
 
 }
-
-@WorkerThread
-fun requestBackup(context: Context) {
-    val backupManager: IBackupManager = get().get()
-    if (backupManager.isBackupEnabled) {
-        val packageService: PackageService = get().get()
-        val packages = packageService.eligiblePackages
-        val appTotals = packageService.expectedAppTotals
-
-        val result = try {
-            Log.d(TAG, "Backup is enabled, request backup...")
-            val observer = NotificationBackupObserver(context, packages.size, appTotals)
-            backupManager.requestBackup(packages, observer, BackupMonitor(), 0)
-        } catch (e: RemoteException) {
-            Log.e(TAG, "Error during backup: ", e)
-            val nm: BackupNotificationManager = get().get()
-            nm.onBackupError()
-        }
-        if (result == BackupManager.SUCCESS) {
-            Log.i(TAG, "Backup succeeded ")
-        } else {
-            Log.e(TAG, "Backup failed: $result")
-        }
-    } else {
-        Log.i(TAG, "Backup is not enabled")
-    }
-}
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
deleted file mode 100644
index bf7d3272..00000000
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package com.stevesoltys.seedvault.transport.backup
-
-import org.koin.android.ext.koin.androidContext
-import org.koin.dsl.module
-
-val backupModule = module {
-    single { InputFactory() }
-    single {
-        PackageService(
-            context = androidContext(),
-            backupManager = get(),
-            settingsManager = get(),
-            plugin = get()
-        )
-    }
-    single {
-        ApkBackup(
-            pm = androidContext().packageManager,
-            crypto = get(),
-            settingsManager = get(),
-            metadataManager = get()
-        )
-    }
-    single<KvDbManager> { KvDbManagerImpl(androidContext()) }
-    single {
-        KVBackup(
-            plugin = get(),
-            settingsManager = get(),
-            inputFactory = get(),
-            crypto = get(),
-            dbManager = get()
-        )
-    }
-    single {
-        FullBackup(
-            plugin = get(),
-            settingsManager = get(),
-            inputFactory = get(),
-            crypto = get()
-        )
-    }
-    single {
-        BackupCoordinator(
-            context = androidContext(),
-            plugin = get(),
-            kv = get(),
-            full = get(),
-            apkBackup = get(),
-            clock = get(),
-            packageService = get(),
-            metadataManager = get(),
-            settingsManager = get(),
-            nm = get()
-        )
-    }
-}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivityBase.kt
similarity index 93%
rename from app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivity.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivityBase.kt
index 40d35fba..4c79bef9 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivity.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/BackupActivityBase.kt
@@ -6,7 +6,7 @@ import androidx.appcompat.app.AppCompatActivity
 import androidx.fragment.app.Fragment
 import com.stevesoltys.seedvault.R
 
-abstract class BackupActivity : AppCompatActivity() {
+abstract class BackupActivityBase : AppCompatActivity() {
 
     @CallSuper
     override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/files/FileSelectionFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/files/FileSelectionFragment.kt
index cde9e8dc..21e5b7e8 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/files/FileSelectionFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/files/FileSelectionFragment.kt
@@ -5,7 +5,7 @@ import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.settings.SettingsViewModel
+import com.stevesoltys.seedvault.ui.settings.SettingsViewModel
 import org.calyxos.backup.storage.ui.backup.BackupContentFragment
 import org.koin.androidx.viewmodel.ext.android.sharedViewModel
 import org.koin.androidx.viewmodel.ext.android.viewModel
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/LiveEvent.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/liveevent/LiveEvent.kt
similarity index 89%
rename from app/src/main/java/com/stevesoltys/seedvault/ui/LiveEvent.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/liveevent/LiveEvent.kt
index 7dac0f91..a06fdb72 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/LiveEvent.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/liveevent/LiveEvent.kt
@@ -1,9 +1,9 @@
-package com.stevesoltys.seedvault.ui
+package com.stevesoltys.seedvault.ui.liveevent
 
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.Observer
-import com.stevesoltys.seedvault.ui.LiveEvent.ConsumableEvent
+import com.stevesoltys.seedvault.ui.liveevent.LiveEvent.ConsumableEvent
 
 open class LiveEvent<T> : LiveData<ConsumableEvent<T>>() {
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/LiveEventHandler.java b/app/src/main/java/com/stevesoltys/seedvault/ui/liveevent/LiveEventHandler.java
similarity index 57%
rename from app/src/main/java/com/stevesoltys/seedvault/ui/LiveEventHandler.java
rename to app/src/main/java/com/stevesoltys/seedvault/ui/liveevent/LiveEventHandler.java
index 858a5ef9..9be03cba 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/LiveEventHandler.java
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/liveevent/LiveEventHandler.java
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.ui;
+package com.stevesoltys.seedvault.ui.liveevent;
 
 public interface LiveEventHandler<T> {
     void onEvent(T t);
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/MutableLiveEvent.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/liveevent/MutableLiveEvent.kt
similarity index 82%
rename from app/src/main/java/com/stevesoltys/seedvault/ui/MutableLiveEvent.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/liveevent/MutableLiveEvent.kt
index 993a2c18..5ffd0422 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/MutableLiveEvent.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/liveevent/MutableLiveEvent.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.ui
+package com.stevesoltys.seedvault.ui.liveevent
 
 class MutableLiveEvent<T> : LiveEvent<T>() {
 
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 35b412fa..d1e12e87 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
@@ -20,12 +20,12 @@ import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
 import androidx.core.app.NotificationCompat.PRIORITY_HIGH
 import androidx.core.app.NotificationCompat.PRIORITY_LOW
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.restore.ACTION_RESTORE_ERROR_UNINSTALL
-import com.stevesoltys.seedvault.restore.EXTRA_PACKAGE_NAME
-import com.stevesoltys.seedvault.restore.REQUEST_CODE_UNINSTALL
-import com.stevesoltys.seedvault.settings.ACTION_APP_STATUS_LIST
-import com.stevesoltys.seedvault.settings.SettingsActivity
-import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals
+import com.stevesoltys.seedvault.ui.restore.ACTION_RESTORE_ERROR_UNINSTALL
+import com.stevesoltys.seedvault.ui.restore.EXTRA_PACKAGE_NAME
+import com.stevesoltys.seedvault.ui.restore.REQUEST_CODE_UNINSTALL
+import com.stevesoltys.seedvault.ui.settings.ACTION_APP_STATUS_LIST
+import com.stevesoltys.seedvault.ui.settings.SettingsActivity
+import com.stevesoltys.seedvault.service.app.ExpectedAppTotals
 
 private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
 private const val CHANNEL_ID_ERROR = "NotificationError"
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 d35971fc..ae23551c 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
@@ -9,8 +9,8 @@ import android.util.Log.INFO
 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.ExpectedAppTotals
+import com.stevesoltys.seedvault.service.app.ExpectedAppTotals
+import com.stevesoltys.seedvault.service.metadata.MetadataService
 import org.koin.core.component.KoinComponent
 import org.koin.core.component.inject
 
@@ -23,7 +23,7 @@ internal class NotificationBackupObserver(
 ) : IBackupObserver.Stub(), KoinComponent {
 
     private val nm: BackupNotificationManager by inject()
-    private val metadataManager: MetadataManager by inject()
+    private val metadataService: MetadataService by inject()
     private var currentPackage: String? = null
     private var numPackages: Int = 0
 
@@ -77,7 +77,7 @@ internal class NotificationBackupObserver(
             Log.i(TAG, "Backup finished $numPackages/$expectedPackages. Status: $status")
         }
         val success = status == 0
-        val numBackedUp = if (success) metadataManager.getPackagesNumBackedUp() else null
+        val numBackedUp = if (success) metadataService.getPackagesNumBackedUp() else null
         nm.onBackupFinished(success, numBackedUp)
     }
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/provision/RequireProvisioningActivity.kt
similarity index 94%
rename from app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningActivity.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/provision/RequireProvisioningActivity.kt
index 393eb2ef..0fa26042 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningActivity.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/provision/RequireProvisioningActivity.kt
@@ -1,10 +1,11 @@
-package com.stevesoltys.seedvault.ui
+package com.stevesoltys.seedvault.ui.provision
 
 import android.content.Intent
 import android.os.Bundle
 import android.util.Log
 import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
 import androidx.annotation.CallSuper
+import com.stevesoltys.seedvault.ui.BackupActivityBase
 import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeActivity
 import com.stevesoltys.seedvault.ui.storage.StorageActivity
 
@@ -19,7 +20,7 @@ private val TAG = RequireProvisioningActivity::class.java.name
  * An Activity that requires the recovery code and the backup location to be set up
  * before starting.
  */
-abstract class RequireProvisioningActivity : BackupActivity() {
+abstract class RequireProvisioningActivity : BackupActivityBase() {
 
     private val recoveryCodeRequest =
         registerForActivityResult(StartActivityForResult()) { result ->
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/provision/RequireProvisioningViewModel.kt
similarity index 68%
rename from app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningViewModel.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/provision/RequireProvisioningViewModel.kt
index 28064441..ebfdfeb9 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/provision/RequireProvisioningViewModel.kt
@@ -1,14 +1,16 @@
-package com.stevesoltys.seedvault.ui
+package com.stevesoltys.seedvault.ui.provision
 
 import android.app.Application
 import androidx.lifecycle.AndroidViewModel
-import com.stevesoltys.seedvault.crypto.KeyManager
-import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.service.crypto.KeyManager
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.ui.liveevent.LiveEvent
+import com.stevesoltys.seedvault.ui.liveevent.MutableLiveEvent
 import com.stevesoltys.seedvault.ui.storage.StorageViewModel
 
 abstract class RequireProvisioningViewModel(
     protected val app: Application,
-    protected val settingsManager: SettingsManager,
+    protected val settingsService: SettingsService,
     protected val keyManager: KeyManager,
 ) : AndroidViewModel(app) {
 
@@ -18,7 +20,7 @@ abstract class RequireProvisioningViewModel(
     internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
     internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
 
-    internal fun validLocationIsSet() = StorageViewModel.validLocationIsSet(app, settingsManager)
+    internal fun validLocationIsSet() = StorageViewModel.validLocationIsSet(app, settingsService)
 
     internal fun recoveryCodeIsSet() = keyManager.hasBackupKey()
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeActivity.kt
index 5923a145..60704696 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeActivity.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeActivity.kt
@@ -5,11 +5,11 @@ import android.view.MenuItem
 import android.view.WindowManager.LayoutParams.FLAG_SECURE
 import com.stevesoltys.seedvault.R
 import com.stevesoltys.seedvault.isDebugBuild
-import com.stevesoltys.seedvault.ui.BackupActivity
-import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
+import com.stevesoltys.seedvault.ui.BackupActivityBase
+import com.stevesoltys.seedvault.ui.provision.INTENT_EXTRA_IS_RESTORE
 import org.koin.androidx.viewmodel.ext.android.viewModel
 
-class RecoveryCodeActivity : BackupActivity() {
+class RecoveryCodeActivity : BackupActivityBase() {
 
     private val viewModel: RecoveryCodeViewModel by viewModel()
 
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 5dbc82ef..0bf30c01 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
@@ -10,12 +10,12 @@ import cash.z.ecc.android.bip39.Mnemonics.InvalidWordException
 import cash.z.ecc.android.bip39.Mnemonics.WordCountException
 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.service.crypto.CryptoService
+import com.stevesoltys.seedvault.service.crypto.KeyManager
 import com.stevesoltys.seedvault.transport.TRANSPORT_ID
-import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
-import com.stevesoltys.seedvault.ui.LiveEvent
-import com.stevesoltys.seedvault.ui.MutableLiveEvent
+import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
+import com.stevesoltys.seedvault.ui.liveevent.LiveEvent
+import com.stevesoltys.seedvault.ui.liveevent.MutableLiveEvent
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.GlobalScope
@@ -29,17 +29,17 @@ private val TAG = RecoveryCodeViewModel::class.java.simpleName
 
 internal class RecoveryCodeViewModel(
     app: App,
-    private val crypto: Crypto,
+    private val cryptoService: CryptoService,
     private val keyManager: KeyManager,
     private val backupManager: IBackupManager,
-    private val backupCoordinator: BackupCoordinator,
+    private val backupCoordinatorService: BackupCoordinatorService,
     private val notificationManager: BackupNotificationManager,
     private val storageBackup: StorageBackup,
 ) : AndroidViewModel(app) {
 
     internal val wordList: List<CharArray> by lazy {
         // we use our own entropy to not having to trust the library to use SecureRandom
-        val entropy = crypto.getRandomBytes(Mnemonics.WordCount.COUNT_12.bitLength / 8)
+        val entropy = cryptoService.getRandomBytes(Mnemonics.WordCount.COUNT_12.bitLength / 8)
         // create the words from the entropy
         Mnemonics.MnemonicCode(entropy).words
     }
@@ -73,7 +73,7 @@ internal class RecoveryCodeViewModel(
     fun verifyExistingCode(input: List<CharSequence>) {
         // we validate the code again, just in case
         val seed = validateCode(input).toSeed()
-        val verified = crypto.verifyBackupKey(seed)
+        val verified = cryptoService.verifyBackupKey(seed)
         // store main key at this opportunity if it is still missing
         if (verified && !keyManager.hasMainKey()) keyManager.storeMainKey(seed)
         mExistingCodeChecked.setEvent(verified)
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestorableBackup.kt
similarity index 77%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestorableBackup.kt
index 2c3abb3c..73eb1ccd 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestorableBackup.kt
@@ -1,7 +1,7 @@
-package com.stevesoltys.seedvault.restore
+package com.stevesoltys.seedvault.ui.restore
 
-import com.stevesoltys.seedvault.metadata.BackupMetadata
-import com.stevesoltys.seedvault.metadata.PackageMetadataMap
+import com.stevesoltys.seedvault.service.metadata.BackupMetadata
+import com.stevesoltys.seedvault.service.metadata.PackageMetadataMap
 
 data class RestorableBackup(val backupMetadata: BackupMetadata) {
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreActivity.kt
similarity index 74%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreActivity.kt
index e0afb7b0..de4b251b 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreActivity.kt
@@ -1,15 +1,15 @@
-package com.stevesoltys.seedvault.restore
+package com.stevesoltys.seedvault.ui.restore
 
 import android.os.Bundle
 import androidx.annotation.CallSuper
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
-import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
-import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
-import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED
-import com.stevesoltys.seedvault.restore.install.InstallProgressFragment
-import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
-import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
+import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_APPS
+import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_BACKUP
+import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES
+import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES_STARTED
+import com.stevesoltys.seedvault.ui.restore.apk.InstallProgressFragment
+import com.stevesoltys.seedvault.ui.provision.RequireProvisioningActivity
+import com.stevesoltys.seedvault.ui.provision.RequireProvisioningViewModel
 import org.koin.androidx.viewmodel.ext.android.viewModel
 
 class RestoreActivity : RequireProvisioningActivity() {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreErrorBroadcastReceiver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreErrorBroadcastReceiver.kt
similarity index 96%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/RestoreErrorBroadcastReceiver.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreErrorBroadcastReceiver.kt
index 1b6dc87b..f482420b 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreErrorBroadcastReceiver.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreErrorBroadcastReceiver.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore
+package com.stevesoltys.seedvault.ui.restore
 
 import android.content.BroadcastReceiver
 import android.content.Context
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreFilesFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreFilesFragment.kt
similarity index 97%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/RestoreFilesFragment.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreFilesFragment.kt
index 531ce034..ca25fe39 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreFilesFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreFilesFragment.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore
+package com.stevesoltys.seedvault.ui.restore
 
 import android.app.Activity.RESULT_OK
 import android.os.Bundle
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreProgressAdapter.kt
similarity index 95%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreProgressAdapter.kt
index 802004af..0d323bd8 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreProgressAdapter.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore
+package com.stevesoltys.seedvault.ui.restore
 
 import android.content.pm.PackageManager.NameNotFoundException
 import android.view.LayoutInflater
@@ -8,7 +8,7 @@ import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.RecyclerView.Adapter
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.restore.RestoreProgressAdapter.PackageViewHolder
+import com.stevesoltys.seedvault.ui.restore.RestoreProgressAdapter.PackageViewHolder
 import com.stevesoltys.seedvault.ui.AppBackupState
 import com.stevesoltys.seedvault.ui.AppViewHolder
 import java.util.LinkedList
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreProgressFragment.kt
similarity index 98%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreProgressFragment.kt
index 17bf9fc6..78969011 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreProgressFragment.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore
+package com.stevesoltys.seedvault.ui.restore
 
 import android.os.Bundle
 import android.view.LayoutInflater
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreSetAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreSetAdapter.kt
similarity index 93%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/RestoreSetAdapter.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreSetAdapter.kt
index f7e7cbb4..3a29bded 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreSetAdapter.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreSetAdapter.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore
+package com.stevesoltys.seedvault.ui.restore
 
 import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE
 import android.text.format.DateUtils.HOUR_IN_MILLIS
@@ -10,7 +10,7 @@ import android.widget.TextView
 import androidx.recyclerview.widget.RecyclerView.Adapter
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.restore.RestoreSetAdapter.RestoreSetViewHolder
+import com.stevesoltys.seedvault.ui.restore.RestoreSetAdapter.RestoreSetViewHolder
 
 internal class RestoreSetAdapter(
     private val listener: RestorableBackupClickListener,
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreSetFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreSetFragment.kt
similarity index 98%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/RestoreSetFragment.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreSetFragment.kt
index 6959565c..3d7b7536 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreSetFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreSetFragment.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore
+package com.stevesoltys.seedvault.ui.restore
 
 import android.os.Bundle
 import android.view.LayoutInflater
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreViewModel.kt
similarity index 89%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreViewModel.kt
index 9992a15c..5ad3c363 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/RestoreViewModel.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore
+package com.stevesoltys.seedvault.ui.restore
 
 import android.app.Application
 import android.app.backup.BackupManager
@@ -18,28 +18,28 @@ import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.Transformations.switchMap
 import androidx.lifecycle.asLiveData
 import androidx.lifecycle.viewModelScope
-import com.stevesoltys.seedvault.BackupMonitor
+import com.stevesoltys.seedvault.service.app.BackupManagerOperationMonitor
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.crypto.KeyManager
-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.QUOTA_EXCEEDED
-import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
-import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
-import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
-import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
-import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
-import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED
-import com.stevesoltys.seedvault.restore.install.ApkRestore
-import com.stevesoltys.seedvault.restore.install.InstallIntentCreator
-import com.stevesoltys.seedvault.restore.install.InstallResult
-import com.stevesoltys.seedvault.restore.install.isInstalled
-import com.stevesoltys.seedvault.settings.SettingsManager
-import com.stevesoltys.seedvault.storage.StorageRestoreService
+import com.stevesoltys.seedvault.service.crypto.KeyManager
+import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
+import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.QUOTA_EXCEEDED
+import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
+import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
+import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_APPS
+import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_BACKUP
+import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES
+import com.stevesoltys.seedvault.ui.restore.DisplayFragment.RESTORE_FILES_STARTED
+import com.stevesoltys.seedvault.ui.restore.apk.ApkRestore
+import com.stevesoltys.seedvault.ui.restore.apk.InstallIntentCreator
+import com.stevesoltys.seedvault.ui.restore.apk.InstallResult
+import com.stevesoltys.seedvault.ui.restore.apk.isInstalled
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.service.file.restore.FileRestoreService
 import com.stevesoltys.seedvault.transport.TRANSPORT_ID
-import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
+import com.stevesoltys.seedvault.service.app.restore.coordinator.RestoreCoordinator
 import com.stevesoltys.seedvault.ui.AppBackupState
 import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
 import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED
@@ -49,9 +49,9 @@ import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_QUOTA_EXCEEDED
 import com.stevesoltys.seedvault.ui.AppBackupState.IN_PROGRESS
 import com.stevesoltys.seedvault.ui.AppBackupState.NOT_YET_BACKED_UP
 import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
-import com.stevesoltys.seedvault.ui.LiveEvent
-import com.stevesoltys.seedvault.ui.MutableLiveEvent
-import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
+import com.stevesoltys.seedvault.ui.liveevent.LiveEvent
+import com.stevesoltys.seedvault.ui.liveevent.MutableLiveEvent
+import com.stevesoltys.seedvault.ui.provision.RequireProvisioningViewModel
 import com.stevesoltys.seedvault.ui.notification.getAppName
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
@@ -74,20 +74,20 @@ internal const val PACKAGES_PER_CHUNK = 100
 
 internal class RestoreViewModel(
     app: Application,
-    settingsManager: SettingsManager,
+    settingsService: SettingsService,
     keyManager: KeyManager,
     private val backupManager: IBackupManager,
     private val restoreCoordinator: RestoreCoordinator,
     private val apkRestore: ApkRestore,
     storageBackup: StorageBackup,
     private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
-) : RequireProvisioningViewModel(app, settingsManager, keyManager),
+) : RequireProvisioningViewModel(app, settingsService, keyManager),
     RestorableBackupClickListener, SnapshotViewModel {
 
     override val isRestoreOperation = true
 
     private var session: IRestoreSession? = null
-    private val monitor = BackupMonitor()
+    private val monitor = BackupManagerOperationMonitor()
 
     private val mDisplayFragment = MutableLiveEvent<DisplayFragment>()
     internal val displayFragment: LiveEvent<DisplayFragment> = mDisplayFragment
@@ -193,8 +193,8 @@ internal class RestoreViewModel(
 
         // if we had no token before (i.e. restore from setup wizard),
         // use the token of the current restore set from now on
-        if (settingsManager.getToken() == null) {
-            settingsManager.setNewToken(token)
+        if (settingsService.getToken() == null) {
+            settingsService.setNewToken(token)
         }
 
         // start a new restore session
@@ -316,7 +316,7 @@ internal class RestoreViewModel(
         private val restorableBackup: RestorableBackup,
         private val session: IRestoreSession,
         private val packages: List<String>,
-        private val monitor: BackupMonitor,
+        private val monitor: BackupManagerOperationMonitor,
     ) : IRestoreObserver.Stub() {
 
         /**
@@ -441,7 +441,7 @@ internal class RestoreViewModel(
 
     @UiThread
     internal fun startFilesRestore(item: SnapshotItem) {
-        val i = Intent(app, StorageRestoreService::class.java)
+        val i = Intent(app, FileRestoreService::class.java)
         i.putExtra(EXTRA_USER_ID, item.storedSnapshot.userId)
         i.putExtra(EXTRA_TIMESTAMP_START, item.time)
         app.startForegroundService(i)
diff --git a/app/src/main/java/com/stevesoltys/seedvault/SecretCodeReceiver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/SecretCodeReceiver.kt
similarity index 61%
rename from app/src/main/java/com/stevesoltys/seedvault/SecretCodeReceiver.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/SecretCodeReceiver.kt
index 63563609..1f2004be 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/SecretCodeReceiver.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/SecretCodeReceiver.kt
@@ -1,22 +1,23 @@
-package com.stevesoltys.seedvault
+package com.stevesoltys.seedvault.ui.restore
 
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
-import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
 import android.util.Log
-import com.stevesoltys.seedvault.restore.RestoreActivity
-
-private val TAG = BroadcastReceiver::class.java.simpleName
-private const val RESTORE_SECRET_CODE = "7378673"
 
 class SecretCodeReceiver : BroadcastReceiver() {
 
+    companion object {
+        private val TAG = BroadcastReceiver::class.java.simpleName
+
+        private const val RESTORE_SECRET_CODE = "7378673"
+    }
+
     override fun onReceive(context: Context, intent: Intent) {
         if (intent.data?.host != RESTORE_SECRET_CODE) return
         Log.d(TAG, "Restore secret code received.")
         val i = Intent(context, RestoreActivity::class.java).apply {
-            flags = FLAG_ACTIVITY_NEW_TASK
+            flags = Intent.FLAG_ACTIVITY_NEW_TASK
         }
         context.startActivity(i)
     }
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkInstaller.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/ApkInstaller.kt
similarity index 96%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkInstaller.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/ApkInstaller.kt
index 4affa6d9..a98089b5 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkInstaller.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/ApkInstaller.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore.install
+package com.stevesoltys.seedvault.ui.restore.apk
 
 import android.annotation.SuppressLint
 import android.app.PendingIntent
@@ -20,8 +20,8 @@ import android.content.pm.PackageInstaller.SessionParams
 import android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL
 import android.content.pm.PackageManager
 import android.util.Log
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.FAILED
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.SUCCEEDED
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/ApkRestore.kt
similarity index 90%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/ApkRestore.kt
index 59600b3d..63dd4720 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/ApkRestore.kt
@@ -1,23 +1,23 @@
-package com.stevesoltys.seedvault.restore.install
+package com.stevesoltys.seedvault.ui.restore.apk
 
 import android.content.Context
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.GET_SIGNATURES
 import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
 import android.util.Log
-import com.stevesoltys.seedvault.crypto.Crypto
-import com.stevesoltys.seedvault.metadata.ApkSplit
-import com.stevesoltys.seedvault.metadata.PackageMetadata
-import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
-import com.stevesoltys.seedvault.plugins.StoragePlugin
-import com.stevesoltys.seedvault.restore.RestorableBackup
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
-import com.stevesoltys.seedvault.transport.backup.copyStreamsAndGetHash
-import com.stevesoltys.seedvault.transport.backup.getSignatures
-import com.stevesoltys.seedvault.transport.backup.isSystemApp
+import com.stevesoltys.seedvault.service.crypto.CryptoService
+import com.stevesoltys.seedvault.service.metadata.ApkSplit
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.ui.restore.RestorableBackup
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.FAILED_SYSTEM_APP
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.IN_PROGRESS
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.QUEUED
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.SUCCEEDED
+import com.stevesoltys.seedvault.service.app.backup.apk.copyStreamsAndGetHash
+import com.stevesoltys.seedvault.service.app.backup.apk.getSignatures
+import com.stevesoltys.seedvault.service.app.isSystemApp
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.flow.FlowCollector
 import kotlinx.coroutines.flow.flow
@@ -31,7 +31,7 @@ internal class ApkRestore(
     private val storagePlugin: StoragePlugin,
     @Suppress("Deprecation")
     private val legacyStoragePlugin: LegacyStoragePlugin,
-    private val crypto: Crypto,
+    private val cryptoService: CryptoService,
     private val splitCompatChecker: ApkSplitCompatibilityChecker,
     private val apkInstaller: ApkInstaller,
 ) {
@@ -222,7 +222,7 @@ internal class ApkRestore(
             @Suppress("Deprecation")
             legacyStoragePlugin.getApkInputStream(token, packageName, suffix)
         } else {
-            val name = crypto.getNameForApk(salt, packageName, suffix)
+            val name = cryptoService.getNameForApk(salt, packageName, suffix)
             storagePlugin.getInputStream(token, name)
         }
         val sha256 = copyStreamsAndGetHash(inputStream, cachedApk.outputStream())
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkSplitCompatibilityChecker.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/ApkSplitCompatibilityChecker.kt
similarity index 98%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkSplitCompatibilityChecker.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/ApkSplitCompatibilityChecker.kt
index a7be09df..e3bea2fd 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkSplitCompatibilityChecker.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/ApkSplitCompatibilityChecker.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore.install
+package com.stevesoltys.seedvault.ui.restore.apk
 
 import android.util.Log
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/DeviceInfo.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/DeviceInfo.kt
similarity index 95%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/install/DeviceInfo.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/DeviceInfo.kt
index 614e2c87..1e72768e 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/DeviceInfo.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/DeviceInfo.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore.install
+package com.stevesoltys.seedvault.ui.restore.apk
 
 import android.content.Context
 import android.os.Build
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallIntentCreator.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallIntentCreator.kt
similarity index 97%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallIntentCreator.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallIntentCreator.kt
index 1f9b13ae..62f0b708 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallIntentCreator.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallIntentCreator.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore.install
+package com.stevesoltys.seedvault.ui.restore.apk
 
 import android.content.Intent
 import android.content.Intent.ACTION_VIEW
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallModule.kt
similarity index 86%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallModule.kt
index 33e640b3..d9b391fc 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallModule.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore.install
+package com.stevesoltys.seedvault.ui.restore.apk
 
 import org.koin.android.ext.koin.androidContext
 import org.koin.dsl.module
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallProgressAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallProgressAdapter.kt
similarity index 91%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallProgressAdapter.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallProgressAdapter.kt
index b3cc5cab..b08ab4b9 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallProgressAdapter.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallProgressAdapter.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore.install
+package com.stevesoltys.seedvault.ui.restore.apk
 
 import android.view.LayoutInflater
 import android.view.View
@@ -10,11 +10,11 @@ import androidx.recyclerview.widget.RecyclerView.Adapter
 import androidx.recyclerview.widget.SortedList
 import androidx.recyclerview.widget.SortedListAdapterCallback
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.FAILED
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.FAILED_SYSTEM_APP
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.IN_PROGRESS
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.QUEUED
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.SUCCEEDED
 import com.stevesoltys.seedvault.ui.AppViewHolder
 import com.stevesoltys.seedvault.ui.notification.getAppName
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallProgressFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallProgressFragment.kt
similarity index 98%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallProgressFragment.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallProgressFragment.kt
index 62948cbc..70e034c6 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallProgressFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallProgressFragment.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.restore.install
+package com.stevesoltys.seedvault.ui.restore.apk
 
 import android.content.ActivityNotFoundException
 import android.content.Context
@@ -18,7 +18,7 @@ import androidx.fragment.app.Fragment
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.restore.RestoreViewModel
+import com.stevesoltys.seedvault.ui.restore.RestoreViewModel
 import org.koin.androidx.viewmodel.ext.android.sharedViewModel
 
 class InstallProgressFragment : Fragment(), InstallItemListener {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallResult.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallResult.kt
similarity index 92%
rename from app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallResult.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallResult.kt
index 982311e7..ce1e54c8 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallResult.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/restore/apk/InstallResult.kt
@@ -1,11 +1,11 @@
-package com.stevesoltys.seedvault.restore.install
+package com.stevesoltys.seedvault.ui.restore.apk
 
 import android.content.pm.PackageManager
 import android.graphics.drawable.Drawable
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.FAILED
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.IN_PROGRESS
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.QUEUED
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.SUCCEEDED
 import java.util.concurrent.ConcurrentHashMap
 
 internal interface InstallResult {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AboutDialogFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/AboutDialogFragment.kt
similarity index 93%
rename from app/src/main/java/com/stevesoltys/seedvault/settings/AboutDialogFragment.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/settings/AboutDialogFragment.kt
index 81738a29..1afb4aa0 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/AboutDialogFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/AboutDialogFragment.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.settings
+package com.stevesoltys.seedvault.ui.settings
 
 import android.os.Bundle
 import android.text.method.LinkMovementMethod
@@ -8,7 +8,7 @@ import android.view.ViewGroup
 import android.widget.TextView
 import androidx.fragment.app.Fragment
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.transport.backup.PackageService
+import com.stevesoltys.seedvault.service.app.PackageService
 import org.koin.android.ext.android.inject
 
 class AboutDialogFragment : Fragment() {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/AppListRetriever.kt
similarity index 89%
rename from app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/settings/AppListRetriever.kt
index e185b79b..1e4d6a14 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/AppListRetriever.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.settings
+package com.stevesoltys.seedvault.ui.settings
 
 import android.annotation.StringRes
 import android.content.Context
@@ -8,9 +8,10 @@ import android.util.Log
 import androidx.annotation.WorkerThread
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.metadata.MetadataManager
-import com.stevesoltys.seedvault.metadata.PackageState
-import com.stevesoltys.seedvault.transport.backup.PackageService
+import com.stevesoltys.seedvault.service.metadata.MetadataService
+import com.stevesoltys.seedvault.service.metadata.PackageState
+import com.stevesoltys.seedvault.service.app.PackageService
+import com.stevesoltys.seedvault.service.settings.SettingsService
 import com.stevesoltys.seedvault.ui.AppBackupState
 import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
 import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED
@@ -47,8 +48,8 @@ class AppSectionTitle(@StringRes val titleRes: Int) : AppListItem()
 internal class AppListRetriever(
     private val context: Context,
     private val packageService: PackageService,
-    private val settingsManager: SettingsManager,
-    private val metadataManager: MetadataManager,
+    private val settingsService: SettingsService,
+    private val metadataService: MetadataService,
 ) {
 
     private val pm: PackageManager = context.packageManager
@@ -75,7 +76,7 @@ internal class AppListRetriever(
             Pair(PACKAGE_NAME_CONTACTS, R.string.backup_contacts)
         )
         return specialPackages.map { (packageName, stringId) ->
-            val metadata = metadataManager.getPackageMetadata(packageName)
+            val metadata = metadataService.getPackageMetadata(packageName)
             val status = if (packageName == PACKAGE_NAME_CONTACTS && metadata?.state == null) {
                 // handle local contacts backup specially as it might not be installed
                 if (packageService.getVersionName(packageName) == null) FAILED_NOT_INSTALLED
@@ -83,7 +84,7 @@ internal class AppListRetriever(
             } else metadata?.state.toAppBackupState()
             AppStatus(
                 packageName = packageName,
-                enabled = settingsManager.isBackupEnabled(packageName),
+                enabled = settingsService.isBackupEnabled(packageName),
                 icon = getIcon(packageName),
                 name = context.getString(stringId),
                 time = metadata?.time ?: 0,
@@ -96,7 +97,7 @@ internal class AppListRetriever(
     private fun getUserApps(): List<AppStatus> {
         val locale = Locale.getDefault()
         return packageService.userApps.map {
-            val metadata = metadataManager.getPackageMetadata(it.packageName)
+            val metadata = metadataService.getPackageMetadata(it.packageName)
             val time = metadata?.time ?: 0
             val status = metadata?.state.toAppBackupState()
             if (status == NOT_YET_BACKED_UP) {
@@ -107,7 +108,7 @@ internal class AppListRetriever(
             }
             AppStatus(
                 packageName = it.packageName,
-                enabled = settingsManager.isBackupEnabled(it.packageName),
+                enabled = settingsService.isBackupEnabled(it.packageName),
                 icon = getIcon(it.packageName),
                 name = getAppName(context, it.packageName).toString(),
                 time = time,
@@ -121,7 +122,7 @@ internal class AppListRetriever(
         return packageService.userNotAllowedApps.map {
             AppStatus(
                 packageName = it.packageName,
-                enabled = settingsManager.isBackupEnabled(it.packageName),
+                enabled = settingsService.isBackupEnabled(it.packageName),
                 icon = getIcon(it.packageName),
                 name = getAppName(context, it.packageName).toString(),
                 time = 0,
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/AppStatusAdapter.kt
similarity index 98%
rename from app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/settings/AppStatusAdapter.kt
index 5536f6b5..ce15ff1c 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/AppStatusAdapter.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.settings
+package com.stevesoltys.seedvault.ui.settings
 
 import android.content.Intent
 import android.net.Uri
@@ -21,7 +21,7 @@ import com.stevesoltys.seedvault.R
 import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED
 import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
 import com.stevesoltys.seedvault.ui.AppViewHolder
-import com.stevesoltys.seedvault.ui.toRelativeTime
+import com.stevesoltys.seedvault.util.toRelativeTime
 
 internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListener) :
     Adapter<RecyclerView.ViewHolder>() {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/AppStatusFragment.kt
similarity index 98%
rename from app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusFragment.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/settings/AppStatusFragment.kt
index 85c18982..6126b056 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/AppStatusFragment.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.settings
+package com.stevesoltys.seedvault.ui.settings
 
 import android.os.Bundle
 import android.view.LayoutInflater
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/BackupManagerSettings.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/BackupManagerSettings.kt
similarity index 95%
rename from app/src/main/java/com/stevesoltys/seedvault/settings/BackupManagerSettings.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/settings/BackupManagerSettings.kt
index 622693c1..6bec6b16 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/BackupManagerSettings.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/BackupManagerSettings.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.settings
+package com.stevesoltys.seedvault.ui.settings
 
 import android.content.ContentResolver
 import android.provider.Settings
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/ExpertSettingsFragment.kt
similarity index 90%
rename from app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/settings/ExpertSettingsFragment.kt
index 05607375..e5ae4507 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/ExpertSettingsFragment.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.settings
+package com.stevesoltys.seedvault.ui.settings
 
 import android.os.Bundle
 import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
@@ -7,7 +7,8 @@ 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
+import com.stevesoltys.seedvault.service.app.PackageService
+import com.stevesoltys.seedvault.service.settings.PREF_KEY_D2D_BACKUPS
 import org.koin.android.ext.android.inject
 import org.koin.androidx.viewmodel.ext.android.sharedViewModel
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/SettingsActivity.kt
similarity index 93%
rename from app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/settings/SettingsActivity.kt
index 68194e29..0202947e 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/SettingsActivity.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.settings
+package com.stevesoltys.seedvault.ui.settings
 
 import android.os.Bundle
 import androidx.annotation.CallSuper
@@ -6,8 +6,8 @@ import androidx.preference.Preference
 import androidx.preference.PreferenceFragmentCompat
 import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
-import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
+import com.stevesoltys.seedvault.ui.provision.RequireProvisioningActivity
+import com.stevesoltys.seedvault.ui.provision.RequireProvisioningViewModel
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import com.stevesoltys.seedvault.ui.recoverycode.ARG_FOR_NEW_CODE
 import org.koin.android.ext.android.inject
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/SettingsFragment.kt
similarity index 95%
rename from app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/settings/SettingsFragment.kt
index 38aaf805..a00b4ccd 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/SettingsFragment.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.settings
+package com.stevesoltys.seedvault.ui.settings
 
 import android.app.backup.IBackupManager
 import android.content.Intent
@@ -21,8 +21,12 @@ import androidx.preference.PreferenceFragmentCompat
 import androidx.preference.TwoStatePreference
 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.service.settings.PREF_KEY_AUTO_RESTORE
+import com.stevesoltys.seedvault.service.settings.PREF_KEY_BACKUP_APK
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.service.settings.Storage
+import com.stevesoltys.seedvault.ui.restore.RestoreActivity
+import com.stevesoltys.seedvault.util.toRelativeTime
 import org.koin.android.ext.android.inject
 import org.koin.androidx.viewmodel.ext.android.sharedViewModel
 
@@ -31,7 +35,7 @@ private val TAG = SettingsFragment::class.java.name
 class SettingsFragment : PreferenceFragmentCompat() {
 
     private val viewModel: SettingsViewModel by sharedViewModel()
-    private val settingsManager: SettingsManager by inject()
+    private val settingsService: SettingsService by inject()
     private val backupManager: IBackupManager by inject()
 
     private lateinit var backup: TwoStatePreference
@@ -155,7 +159,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
         // we need to re-set the title when returning to this fragment
         activity?.setTitle(R.string.backup)
 
-        storage = settingsManager.getStorage()
+        storage = settingsService.getStorage()
         setBackupEnabledState()
         setBackupLocationSummary()
         setAutoRestoreState()
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/SettingsViewModel.kt
similarity index 86%
rename from app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt
rename to app/src/main/java/com/stevesoltys/seedvault/ui/settings/SettingsViewModel.kt
index 3c190651..cce2c56c 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/settings/SettingsViewModel.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.settings
+package com.stevesoltys.seedvault.ui.settings
 
 import android.app.Application
 import android.app.backup.IBackupManager
@@ -25,14 +25,15 @@ import androidx.lifecycle.liveData
 import androidx.lifecycle.viewModelScope
 import androidx.recyclerview.widget.DiffUtil.calculateDiff
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.crypto.KeyManager
-import com.stevesoltys.seedvault.metadata.MetadataManager
 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.service.app.backup.requestBackup
+import com.stevesoltys.seedvault.service.crypto.KeyManager
+import com.stevesoltys.seedvault.service.file.backup.FileBackupJobService
+import com.stevesoltys.seedvault.service.file.backup.FileBackupService
+import com.stevesoltys.seedvault.service.file.backup.FileBackupService.Companion.EXTRA_START_APP_BACKUP
+import com.stevesoltys.seedvault.service.metadata.MetadataService
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.ui.provision.RequireProvisioningViewModel
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -48,14 +49,14 @@ private const val USER_FULL_DATA_BACKUP_AWARE = "user_full_data_backup_aware"
 
 internal class SettingsViewModel(
     app: Application,
-    settingsManager: SettingsManager,
+    settingsService: SettingsService,
     keyManager: KeyManager,
     private val notificationManager: BackupNotificationManager,
-    private val metadataManager: MetadataManager,
+    private val metadataService: MetadataService,
     private val appListRetriever: AppListRetriever,
     private val storageBackup: StorageBackup,
     private val backupManager: IBackupManager,
-) : RequireProvisioningViewModel(app, settingsManager, keyManager) {
+) : RequireProvisioningViewModel(app, settingsService, keyManager) {
 
     private val contentResolver = app.contentResolver
     private val connectivityManager = app.getSystemService(ConnectivityManager::class.java)
@@ -65,7 +66,7 @@ internal class SettingsViewModel(
     private val mBackupPossible = MutableLiveData(false)
     val backupPossible: LiveData<Boolean> = mBackupPossible
 
-    internal val lastBackupTime = metadataManager.lastBackupTime
+    internal val lastBackupTime = metadataService.lastBackupTime
 
     private val mAppStatusList = switchMap(lastBackupTime) {
         // updates app list when lastBackupTime changes
@@ -106,14 +107,14 @@ internal class SettingsViewModel(
         }
         scope.launch {
             // ensures the lastBackupTime LiveData gets set
-            metadataManager.getLastBackupTime()
+            metadataService.getLastBackupTime()
         }
         onStorageLocationChanged()
         loadFilesSummary()
     }
 
     override fun onStorageLocationChanged() {
-        val storage = settingsManager.getStorage() ?: return
+        val storage = settingsService.getStorage() ?: return
 
         // register storage observer
         try {
@@ -137,7 +138,7 @@ internal class SettingsViewModel(
             networkCallback.registered = true
         }
 
-        if (settingsManager.isStorageBackupEnabled()) {
+        if (settingsService.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,
@@ -146,7 +147,7 @@ internal class SettingsViewModel(
         }
 
         viewModelScope.launch(Dispatchers.IO) {
-            val canDo = settingsManager.canDoBackupNow()
+            val canDo = settingsService.canDoBackupNow()
             mBackupPossible.postValue(canDo)
         }
     }
@@ -166,8 +167,8 @@ internal class SettingsViewModel(
         } else if (!backupManager.isBackupEnabled) {
             Toast.makeText(app, R.string.notification_backup_disabled, LENGTH_LONG).show()
         } else viewModelScope.launch(Dispatchers.IO) {
-            if (settingsManager.isStorageBackupEnabled()) {
-                val i = Intent(app, StorageBackupService::class.java)
+            if (settingsService.isStorageBackupEnabled()) {
+                val i = Intent(app, FileBackupService::class.java)
                 // this starts an app backup afterwards
                 i.putExtra(EXTRA_START_APP_BACKUP, true)
                 startForegroundService(app, i)
@@ -191,7 +192,7 @@ internal class SettingsViewModel(
 
     @UiThread
     fun onAppStatusToggled(status: AppStatus) {
-        settingsManager.onAppBackupStatusChanged(status)
+        settingsService.onAppBackupStatusChanged(status)
     }
 
     @UiThread
@@ -221,10 +222,10 @@ internal class SettingsViewModel(
     }
 
     fun enableStorageBackup() {
-        val storage = settingsManager.getStorage() ?: error("no storage available")
+        val storage = settingsService.getStorage() ?: error("no storage available")
         if (!storage.isUsb) BackupJobService.scheduleJob(
             context = app,
-            jobServiceClass = StorageBackupJobService::class.java,
+            jobServiceClass = FileBackupJobService::class.java,
             periodMillis = HOURS.toMillis(24),
             networkType = if (storage.requiresNetwork) NETWORK_TYPE_UNMETERED
             else NETWORK_TYPE_NONE,
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 4595468b..69a9fbc5 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,10 +10,10 @@ import android.util.Log
 import androidx.annotation.WorkerThread
 import androidx.lifecycle.viewModelScope
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.service.settings.SettingsService
 import com.stevesoltys.seedvault.transport.TRANSPORT_ID
-import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
-import com.stevesoltys.seedvault.transport.requestBackup
+import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
+import com.stevesoltys.seedvault.service.app.backup.requestBackup
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import org.calyxos.backup.storage.api.StorageBackup
@@ -24,10 +24,10 @@ private val TAG = BackupStorageViewModel::class.java.simpleName
 internal class BackupStorageViewModel(
     private val app: Application,
     private val backupManager: IBackupManager,
-    private val backupCoordinator: BackupCoordinator,
+    private val backupCoordinatorService: BackupCoordinatorService,
     private val storageBackup: StorageBackup,
-    settingsManager: SettingsManager,
-) : StorageViewModel(app, settingsManager) {
+    settingsService: SettingsService,
+) : StorageViewModel(app, settingsService) {
 
     override val isRestoreOperation = false
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt
index 916ca3e0..e7c69d47 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt
@@ -5,9 +5,9 @@ import android.net.Uri
 import android.util.Log
 import androidx.lifecycle.viewModelScope
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.plugins.StoragePlugin
-import com.stevesoltys.seedvault.plugins.saf.DIRECTORY_ROOT
-import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.storage.saf.DIRECTORY_ROOT
+import com.stevesoltys.seedvault.service.settings.SettingsService
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import java.io.IOException
@@ -17,8 +17,8 @@ private val TAG = RestoreStorageViewModel::class.java.simpleName
 internal class RestoreStorageViewModel(
     private val app: Application,
     private val storagePlugin: StoragePlugin,
-    settingsManager: SettingsManager,
-) : StorageViewModel(app, settingsManager) {
+    settingsService: SettingsService,
+) : StorageViewModel(app, settingsService) {
 
     override val isRestoreOperation = true
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageActivity.kt
index ca743c6f..56d63ed1 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageActivity.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageActivity.kt
@@ -14,14 +14,14 @@ import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTre
 import androidx.annotation.CallSuper
 import androidx.appcompat.app.AlertDialog
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.ui.BackupActivity
-import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
-import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_SETUP_WIZARD
+import com.stevesoltys.seedvault.ui.BackupActivityBase
+import com.stevesoltys.seedvault.ui.provision.INTENT_EXTRA_IS_RESTORE
+import com.stevesoltys.seedvault.ui.provision.INTENT_EXTRA_IS_SETUP_WIZARD
 import org.koin.androidx.viewmodel.ext.android.getViewModel
 
 private val TAG = StorageActivity::class.java.name
 
-class StorageActivity : BackupActivity() {
+class StorageActivity : BackupActivityBase() {
 
     private lateinit var viewModel: StorageViewModel
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOptionsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOptionsFragment.kt
index fc84248c..1360398c 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOptionsFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOptionsFragment.kt
@@ -23,7 +23,7 @@ import androidx.annotation.RequiresPermission
 import androidx.fragment.app.Fragment
 import androidx.recyclerview.widget.RecyclerView
 import com.stevesoltys.seedvault.R
-import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
+import com.stevesoltys.seedvault.ui.provision.INTENT_EXTRA_IS_RESTORE
 import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
 import org.koin.androidx.viewmodel.ext.android.getSharedViewModel
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageViewModel.kt
index 0d0e5f11..99dbba26 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageViewModel.kt
@@ -14,19 +14,19 @@ import androidx.lifecycle.MutableLiveData
 import com.stevesoltys.seedvault.R
 import com.stevesoltys.seedvault.isMassStorage
 import com.stevesoltys.seedvault.permitDiskReads
-import com.stevesoltys.seedvault.settings.BackupManagerSettings
-import com.stevesoltys.seedvault.settings.FlashDrive
-import com.stevesoltys.seedvault.settings.SettingsManager
-import com.stevesoltys.seedvault.settings.Storage
-import com.stevesoltys.seedvault.ui.LiveEvent
-import com.stevesoltys.seedvault.ui.MutableLiveEvent
+import com.stevesoltys.seedvault.service.settings.FlashDrive
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.service.settings.Storage
+import com.stevesoltys.seedvault.ui.settings.BackupManagerSettings
+import com.stevesoltys.seedvault.ui.liveevent.LiveEvent
+import com.stevesoltys.seedvault.ui.liveevent.MutableLiveEvent
 import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
 
 private val TAG = StorageViewModel::class.java.simpleName
 
 internal abstract class StorageViewModel(
     private val app: Application,
-    protected val settingsManager: SettingsManager,
+    protected val settingsService: SettingsService,
 ) : AndroidViewModel(app), RemovableStorageListener {
 
     private val mStorageOptions = MutableLiveData<List<StorageOption>>()
@@ -43,15 +43,15 @@ internal abstract class StorageViewModel(
 
     internal var isSetupWizard: Boolean = false
     internal val hasStorageSet: Boolean
-        get() = settingsManager.getStorage() != null
+        get() = settingsService.getStorage() != null
     abstract val isRestoreOperation: Boolean
 
     companion object {
         internal fun validLocationIsSet(
             context: Context,
-            settingsManager: SettingsManager,
+            settingsService: SettingsService,
         ): Boolean {
-            val storage = settingsManager.getStorage() ?: return false
+            val storage = settingsService.getStorage() ?: return false
             if (storage.isUsb) return true
             return permitDiskReads {
                 storage.getDocumentFile(context).isDirectory
@@ -113,15 +113,15 @@ internal abstract class StorageViewModel(
     }
 
     protected fun saveStorage(storage: Storage): Boolean {
-        settingsManager.setStorage(storage)
+        settingsService.setStorage(storage)
 
         if (storage.isUsb) {
             Log.d(TAG, "Selected storage is a removable USB device.")
             val wasSaved = saveUsbDevice()
             // reset stored flash drive, if we did not update it
-            if (!wasSaved) settingsManager.setFlashDrive(null)
+            if (!wasSaved) settingsService.setFlashDrive(null)
         } else {
-            settingsManager.setFlashDrive(null)
+            settingsService.setFlashDrive(null)
         }
         BackupManagerSettings.resetDefaults(app.contentResolver)
 
@@ -135,7 +135,7 @@ internal abstract class StorageViewModel(
         manager.deviceList.values.forEach { device ->
             if (device.isMassStorage()) {
                 val flashDrive = FlashDrive.from(device)
-                settingsManager.setFlashDrive(flashDrive)
+                settingsService.setFlashDrive(flashDrive)
                 Log.d(TAG, "Saved flash drive: $flashDrive")
                 return true
             }
diff --git a/app/src/main/java/com/stevesoltys/seedvault/Base64Utils.kt b/app/src/main/java/com/stevesoltys/seedvault/util/Base64Utils.kt
similarity index 90%
rename from app/src/main/java/com/stevesoltys/seedvault/Base64Utils.kt
rename to app/src/main/java/com/stevesoltys/seedvault/util/Base64Utils.kt
index 69571f51..74863128 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/Base64Utils.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/util/Base64Utils.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault
+package com.stevesoltys.seedvault.util
 
 import java.nio.charset.Charset
 import java.util.Base64
diff --git a/app/src/main/java/com/stevesoltys/seedvault/Clock.kt b/app/src/main/java/com/stevesoltys/seedvault/util/TimeSource.kt
similarity index 79%
rename from app/src/main/java/com/stevesoltys/seedvault/Clock.kt
rename to app/src/main/java/com/stevesoltys/seedvault/util/TimeSource.kt
index 3a39ffe1..9e437a61 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/Clock.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/util/TimeSource.kt
@@ -1,9 +1,9 @@
-package com.stevesoltys.seedvault
+package com.stevesoltys.seedvault.util
 
 /**
  * This class only exists, so we can mock the time in tests.
  */
-class Clock {
+class TimeSource {
     /**
      * Returns the current time in milliseconds (Unix time).
      */
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/UiUtils.kt b/app/src/main/java/com/stevesoltys/seedvault/util/UiUtils.kt
similarity index 92%
rename from app/src/main/java/com/stevesoltys/seedvault/ui/UiUtils.kt
rename to app/src/main/java/com/stevesoltys/seedvault/util/UiUtils.kt
index 26378dbf..4dd8350d 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/UiUtils.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/util/UiUtils.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.seedvault.ui
+package com.stevesoltys.seedvault.util
 
 import android.content.Context
 import android.text.format.DateUtils.MINUTE_IN_MILLIS
diff --git a/app/src/sharedTest/java/com/stevesoltys/seedvault/crypto/KeyManagerTestImpl.kt b/app/src/sharedTest/java/com/stevesoltys/seedvault/crypto/KeyManagerTestImpl.kt
index 1bb0c7c5..2bb3d537 100644
--- a/app/src/sharedTest/java/com/stevesoltys/seedvault/crypto/KeyManagerTestImpl.kt
+++ b/app/src/sharedTest/java/com/stevesoltys/seedvault/crypto/KeyManagerTestImpl.kt
@@ -1,5 +1,7 @@
 package com.stevesoltys.seedvault.crypto
 
+import com.stevesoltys.seedvault.service.crypto.KEY_SIZE
+import com.stevesoltys.seedvault.service.crypto.KeyManager
 import javax.crypto.KeyGenerator
 import javax.crypto.SecretKey
 
diff --git a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt
index cdf03aea..d5edac29 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt
@@ -1,18 +1,19 @@
 package com.stevesoltys.seedvault
 
-import com.stevesoltys.seedvault.crypto.CipherFactory
-import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
-import com.stevesoltys.seedvault.crypto.Crypto
-import com.stevesoltys.seedvault.crypto.CryptoImpl
-import com.stevesoltys.seedvault.crypto.KeyManager
+import com.stevesoltys.seedvault.service.crypto.CipherFactory
+import com.stevesoltys.seedvault.service.crypto.CipherFactoryImpl
+import com.stevesoltys.seedvault.service.crypto.CryptoService
+import com.stevesoltys.seedvault.service.crypto.CryptoServiceImpl
+import com.stevesoltys.seedvault.service.crypto.KeyManager
 import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
-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 com.stevesoltys.seedvault.service.header.headerModule
+import com.stevesoltys.seedvault.service.metadata.metadataModule
+import com.stevesoltys.seedvault.service.storage.saf.documentsProviderModule
+import com.stevesoltys.seedvault.ui.restore.apk.installModule
+import com.stevesoltys.seedvault.service.settings.SettingsService
+import com.stevesoltys.seedvault.service.app.backup.backupModule
+import com.stevesoltys.seedvault.service.app.restore.restoreModule
+import com.stevesoltys.seedvault.util.TimeSource
 import org.koin.android.ext.koin.androidContext
 import org.koin.core.context.startKoin
 import org.koin.dsl.module
@@ -22,11 +23,11 @@ class TestApp : App() {
     private val testCryptoModule = module {
         factory<CipherFactory> { CipherFactoryImpl(get()) }
         single<KeyManager> { KeyManagerTestImpl() }
-        single<Crypto> { CryptoImpl(get(), get(), get()) }
+        single<CryptoService> { CryptoServiceImpl(get(), get(), get()) }
     }
     private val appModule = module {
-        single { Clock() }
-        single { SettingsManager(this@TestApp) }
+        single { TimeSource() }
+        single { SettingsService(this@TestApp) }
     }
 
     override fun startKoin() = startKoin {
diff --git a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoImplTest.kt b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoServiceImplTest.kt
similarity index 86%
rename from app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoImplTest.kt
rename to app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoServiceImplTest.kt
index 879ee745..629fc3f5 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoImplTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoServiceImplTest.kt
@@ -2,8 +2,11 @@ package com.stevesoltys.seedvault.crypto
 
 import com.stevesoltys.seedvault.getRandomBase64
 import com.stevesoltys.seedvault.getRandomString
-import com.stevesoltys.seedvault.header.HeaderReaderImpl
-import com.stevesoltys.seedvault.metadata.METADATA_SALT_SIZE
+import com.stevesoltys.seedvault.service.header.HeaderDecodeServiceImpl
+import com.stevesoltys.seedvault.service.metadata.METADATA_SALT_SIZE
+import com.stevesoltys.seedvault.service.crypto.CipherFactory
+import com.stevesoltys.seedvault.service.crypto.CryptoServiceImpl
+import com.stevesoltys.seedvault.service.crypto.KeyManager
 import io.mockk.mockk
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Assertions.assertNotEquals
@@ -15,13 +18,13 @@ import java.io.ByteArrayInputStream
 import java.io.IOException
 
 @TestInstance(PER_METHOD)
-class CryptoImplTest {
+class CryptoServiceImplTest {
 
     private val keyManager = mockk<KeyManager>()
     private val cipherFactory = mockk<CipherFactory>()
-    private val headerReader = HeaderReaderImpl()
+    private val headerReader = HeaderDecodeServiceImpl()
 
-    private val crypto = CryptoImpl(keyManager, cipherFactory, headerReader)
+    private val crypto = CryptoServiceImpl(keyManager, cipherFactory, headerReader)
 
     @Test
     fun `decrypting multiple segments on empty stream throws`() {
diff --git a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoServiceIntegrationTest.kt
similarity index 84%
rename from app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoIntegrationTest.kt
rename to app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoServiceIntegrationTest.kt
index 7eedba18..847947d3 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoIntegrationTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoServiceIntegrationTest.kt
@@ -1,7 +1,9 @@
 package com.stevesoltys.seedvault.crypto
 
 import com.stevesoltys.seedvault.assertReadEquals
-import com.stevesoltys.seedvault.header.HeaderReaderImpl
+import com.stevesoltys.seedvault.service.header.HeaderDecodeServiceImpl
+import com.stevesoltys.seedvault.service.crypto.CipherFactoryImpl
+import com.stevesoltys.seedvault.service.crypto.CryptoServiceImpl
 import org.hamcrest.MatcherAssert.assertThat
 import org.hamcrest.Matchers.equalTo
 import org.hamcrest.Matchers.not
@@ -15,12 +17,12 @@ import java.io.IOException
 import kotlin.random.Random
 
 @TestInstance(PER_METHOD)
-class CryptoIntegrationTest {
+class CryptoServiceIntegrationTest {
 
     private val keyManager = KeyManagerTestImpl()
     private val cipherFactory = CipherFactoryImpl(keyManager)
-    private val headerReader = HeaderReaderImpl()
-    private val crypto = CryptoImpl(keyManager, cipherFactory, headerReader)
+    private val headerReader = HeaderDecodeServiceImpl()
+    private val crypto = CryptoServiceImpl(keyManager, cipherFactory, headerReader)
 
     private val cleartext = Random.nextBytes(Random.nextInt(1, 422300))
 
diff --git a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoTest.kt b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoServiceTest.kt
similarity index 72%
rename from app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoTest.kt
rename to app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoServiceTest.kt
index 3480283e..b3537d3c 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoServiceTest.kt
@@ -3,15 +3,18 @@ package com.stevesoltys.seedvault.crypto
 import com.stevesoltys.seedvault.assertContains
 import com.stevesoltys.seedvault.getRandomByteArray
 import com.stevesoltys.seedvault.getRandomString
-import com.stevesoltys.seedvault.header.HeaderReader
-import com.stevesoltys.seedvault.header.IV_SIZE
-import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE
-import com.stevesoltys.seedvault.header.MAX_PACKAGE_LENGTH_SIZE
-import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
-import com.stevesoltys.seedvault.header.MAX_VERSION_HEADER_SIZE
-import com.stevesoltys.seedvault.header.SegmentHeader
-import com.stevesoltys.seedvault.header.VERSION
-import com.stevesoltys.seedvault.header.VersionHeader
+import com.stevesoltys.seedvault.service.crypto.CipherFactory
+import com.stevesoltys.seedvault.service.crypto.CryptoServiceImpl
+import com.stevesoltys.seedvault.service.crypto.KeyManager
+import com.stevesoltys.seedvault.service.header.HeaderDecodeService
+import com.stevesoltys.seedvault.service.header.IV_SIZE
+import com.stevesoltys.seedvault.service.header.MAX_KEY_LENGTH_SIZE
+import com.stevesoltys.seedvault.service.header.MAX_PACKAGE_LENGTH_SIZE
+import com.stevesoltys.seedvault.service.header.MAX_SEGMENT_LENGTH
+import com.stevesoltys.seedvault.service.header.MAX_VERSION_HEADER_SIZE
+import com.stevesoltys.seedvault.service.header.SegmentHeader
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.header.VersionHeader
 import io.mockk.every
 import io.mockk.mockk
 import org.junit.jupiter.api.Assertions.assertArrayEquals
@@ -29,13 +32,13 @@ import javax.crypto.Cipher
 import kotlin.random.Random
 
 @TestInstance(PER_METHOD)
-class CryptoTest {
+class CryptoServiceTest {
 
     private val keyManager = mockk<KeyManager>()
     private val cipherFactory = mockk<CipherFactory>()
-    private val headerReader = mockk<HeaderReader>()
+    private val headerDecodeService = mockk<HeaderDecodeService>()
 
-    private val crypto = CryptoImpl(keyManager, cipherFactory, headerReader)
+    private val crypto = CryptoServiceImpl(keyManager, cipherFactory, headerDecodeService)
 
     private val cipher = mockk<Cipher>()
 
@@ -59,10 +62,12 @@ class CryptoTest {
 
     @Test
     fun `decrypting header works as expected`() {
-        every { headerReader.readSegmentHeader(versionInputStream) } returns versionSegmentHeader
+        every {
+            headerDecodeService.readSegmentHeader(versionInputStream)
+        } returns versionSegmentHeader
         every { cipherFactory.createDecryptionCipher(iv) } returns cipher
         every { cipher.doFinal(versionCiphertext) } returns cleartext
-        every { headerReader.getVersionHeader(cleartext) } returns versionHeader
+        every { headerDecodeService.getVersionHeader(cleartext) } returns versionHeader
 
         assertEquals(
             versionHeader,
@@ -82,7 +87,9 @@ class CryptoTest {
         val versionInputStream = ByteArrayInputStream(versionCiphertext)
         val versionSegmentHeader = SegmentHeader(size.toShort(), iv)
 
-        every { headerReader.readSegmentHeader(versionInputStream) } returns versionSegmentHeader
+        every {
+            headerDecodeService.readSegmentHeader(versionInputStream)
+        } returns versionSegmentHeader
 
         val e = assertThrows(SecurityException::class.java) {
             crypto.decryptHeader(
@@ -97,10 +104,12 @@ class CryptoTest {
 
     @Test
     fun `decrypting header throws because of different version`() {
-        every { headerReader.readSegmentHeader(versionInputStream) } returns versionSegmentHeader
+        every {
+            headerDecodeService.readSegmentHeader(versionInputStream)
+        } returns versionSegmentHeader
         every { cipherFactory.createDecryptionCipher(iv) } returns cipher
         every { cipher.doFinal(versionCiphertext) } returns cleartext
-        every { headerReader.getVersionHeader(cleartext) } returns versionHeader
+        every { headerDecodeService.getVersionHeader(cleartext) } returns versionHeader
 
         val version = (VERSION + 1).toByte()
         val e = assertThrows(SecurityException::class.java) {
@@ -116,10 +125,12 @@ class CryptoTest {
 
     @Test
     fun `decrypting header throws because of different package name`() {
-        every { headerReader.readSegmentHeader(versionInputStream) } returns versionSegmentHeader
+        every {
+            headerDecodeService.readSegmentHeader(versionInputStream)
+        } returns versionSegmentHeader
         every { cipherFactory.createDecryptionCipher(iv) } returns cipher
         every { cipher.doFinal(versionCiphertext) } returns cleartext
-        every { headerReader.getVersionHeader(cleartext) } returns versionHeader
+        every { headerDecodeService.getVersionHeader(cleartext) } returns versionHeader
 
         val packageName = getRandomString(MAX_PACKAGE_LENGTH_SIZE)
         val e = assertThrows(SecurityException::class.java) {
@@ -135,10 +146,12 @@ class CryptoTest {
 
     @Test
     fun `decrypting header throws because of different key`() {
-        every { headerReader.readSegmentHeader(versionInputStream) } returns versionSegmentHeader
+        every {
+            headerDecodeService.readSegmentHeader(versionInputStream)
+        } returns versionSegmentHeader
         every { cipherFactory.createDecryptionCipher(iv) } returns cipher
         every { cipher.doFinal(versionCiphertext) } returns cleartext
-        every { headerReader.getVersionHeader(cleartext) } returns versionHeader
+        every { headerDecodeService.getVersionHeader(cleartext) } returns versionHeader
 
         val e = assertThrows(SecurityException::class.java) {
             crypto.decryptHeader(
@@ -154,7 +167,7 @@ class CryptoTest {
 
     @Test
     fun `decrypting data segment header works as expected`() {
-        every { headerReader.readSegmentHeader(inputStream) } returns segmentHeader
+        every { headerDecodeService.readSegmentHeader(inputStream) } returns segmentHeader
         every { cipherFactory.createDecryptionCipher(iv) } returns cipher
         every { cipher.doFinal(ciphertext) } returns cleartext
 
@@ -166,7 +179,7 @@ class CryptoTest {
         val inputStream = mockk<InputStream>()
         val buffer = ByteArray(segmentHeader.segmentLength.toInt())
 
-        every { headerReader.readSegmentHeader(inputStream) } returns segmentHeader
+        every { headerDecodeService.readSegmentHeader(inputStream) } returns segmentHeader
         every { inputStream.read(buffer) } returns 0
 
         assertThrows(IOException::class.java) {
@@ -179,7 +192,7 @@ class CryptoTest {
         val inputStream = mockk<InputStream>()
         val buffer = ByteArray(segmentHeader.segmentLength.toInt())
 
-        every { headerReader.readSegmentHeader(inputStream) } returns segmentHeader
+        every { headerDecodeService.readSegmentHeader(inputStream) } returns segmentHeader
         every { inputStream.read(buffer) } returns -1
 
         assertThrows(EOFException::class.java) {
@@ -192,7 +205,7 @@ class CryptoTest {
         val inputStream = mockk<InputStream>()
         val buffer = ByteArray(segmentHeader.segmentLength.toInt())
 
-        every { headerReader.readSegmentHeader(inputStream) } returns segmentHeader
+        every { headerDecodeService.readSegmentHeader(inputStream) } returns segmentHeader
         every { inputStream.read(buffer) } returns buffer.size - 1
 
         assertThrows(IOException::class.java) {
diff --git a/app/src/test/java/com/stevesoltys/seedvault/crypto/KeyManagerImplTest.kt b/app/src/test/java/com/stevesoltys/seedvault/crypto/KeyManagerImplTest.kt
index 7f7ee538..9d83446c 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/crypto/KeyManagerImplTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/crypto/KeyManagerImplTest.kt
@@ -1,6 +1,7 @@
 package com.stevesoltys.seedvault.crypto
 
 import com.stevesoltys.seedvault.getRandomByteArray
+import com.stevesoltys.seedvault.service.crypto.KeyManagerImpl
 import io.mockk.Runs
 import io.mockk.every
 import io.mockk.just
diff --git a/app/src/test/java/com/stevesoltys/seedvault/header/HeaderReaderTest.kt b/app/src/test/java/com/stevesoltys/seedvault/header/HeaderDecodeServiceTest.kt
similarity index 92%
rename from app/src/test/java/com/stevesoltys/seedvault/header/HeaderReaderTest.kt
rename to app/src/test/java/com/stevesoltys/seedvault/header/HeaderDecodeServiceTest.kt
index 7765d1ee..6f1b9315 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/header/HeaderReaderTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/header/HeaderDecodeServiceTest.kt
@@ -1,8 +1,18 @@
 package com.stevesoltys.seedvault.header
 
-import com.stevesoltys.seedvault.Utf8
+import com.stevesoltys.seedvault.util.Utf8
 import com.stevesoltys.seedvault.assertContains
 import com.stevesoltys.seedvault.getRandomString
+import com.stevesoltys.seedvault.service.header.HeaderDecodeServiceImpl
+import com.stevesoltys.seedvault.service.header.IV_SIZE
+import com.stevesoltys.seedvault.service.header.MAX_KEY_LENGTH_SIZE
+import com.stevesoltys.seedvault.service.header.MAX_PACKAGE_LENGTH_SIZE
+import com.stevesoltys.seedvault.service.header.MAX_SEGMENT_LENGTH
+import com.stevesoltys.seedvault.service.header.MAX_VERSION_HEADER_SIZE
+import com.stevesoltys.seedvault.service.header.SEGMENT_HEADER_SIZE
+import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.header.VersionHeader
 import org.junit.jupiter.api.Assertions.assertArrayEquals
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Assertions.assertThrows
@@ -15,9 +25,9 @@ import java.nio.ByteBuffer
 import kotlin.random.Random
 
 @TestInstance(PER_CLASS)
-internal class HeaderReaderTest {
+internal class HeaderDecodeServiceTest {
 
-    private val reader = HeaderReaderImpl()
+    private val reader = HeaderDecodeServiceImpl()
 
     // Version Tests
 
diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReadWriteTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReadWriteTest.kt
index fdd6aff8..73792ee5 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReadWriteTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReadWriteTest.kt
@@ -1,15 +1,20 @@
 package com.stevesoltys.seedvault.metadata
 
-import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
-import com.stevesoltys.seedvault.crypto.CryptoImpl
-import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES
+import com.stevesoltys.seedvault.service.crypto.CipherFactoryImpl
+import com.stevesoltys.seedvault.service.crypto.CryptoServiceImpl
+import com.stevesoltys.seedvault.service.crypto.KEY_SIZE_BYTES
 import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
 import com.stevesoltys.seedvault.getRandomBase64
 import com.stevesoltys.seedvault.getRandomString
-import com.stevesoltys.seedvault.header.HeaderReaderImpl
-import com.stevesoltys.seedvault.header.VERSION
-import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
-import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
+import com.stevesoltys.seedvault.service.header.HeaderDecodeServiceImpl
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.metadata.BackupMetadata
+import com.stevesoltys.seedvault.service.metadata.BackupType
+import com.stevesoltys.seedvault.service.metadata.MetadataReaderImpl
+import com.stevesoltys.seedvault.service.metadata.MetadataWriterImpl
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.TestInstance
@@ -27,11 +32,11 @@ internal class MetadataReadWriteTest {
     )
     private val keyManager = KeyManagerTestImpl(secretKey)
     private val cipherFactory = CipherFactoryImpl(keyManager)
-    private val headerReader = HeaderReaderImpl()
-    private val cryptoImpl = CryptoImpl(keyManager, cipherFactory, headerReader)
+    private val headerReader = HeaderDecodeServiceImpl()
+    private val cryptoServiceImpl = CryptoServiceImpl(keyManager, cipherFactory, headerReader)
 
-    private val writer = MetadataWriterImpl(cryptoImpl)
-    private val reader = MetadataReaderImpl(cryptoImpl)
+    private val writer = MetadataWriterImpl(cryptoServiceImpl)
+    private val reader = MetadataReaderImpl(cryptoServiceImpl)
 
     private val packages = HashMap<String, PackageMetadata>().apply {
         put(getRandomString(), PackageMetadata(Random.nextLong(), APK_AND_DATA, BackupType.FULL))
diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReaderTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReaderTest.kt
index 140d5915..de837bcf 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReaderTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReaderTest.kt
@@ -1,11 +1,29 @@
 package com.stevesoltys.seedvault.metadata
 
-import com.stevesoltys.seedvault.Utf8
-import com.stevesoltys.seedvault.crypto.Crypto
+import com.stevesoltys.seedvault.util.Utf8
+import com.stevesoltys.seedvault.service.crypto.CryptoService
 import com.stevesoltys.seedvault.getRandomBase64
 import com.stevesoltys.seedvault.getRandomString
-import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
-import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
+import com.stevesoltys.seedvault.service.metadata.BackupMetadata
+import com.stevesoltys.seedvault.service.metadata.BackupType
+import com.stevesoltys.seedvault.service.metadata.JSON_METADATA
+import com.stevesoltys.seedvault.service.metadata.JSON_METADATA_SDK_INT
+import com.stevesoltys.seedvault.service.metadata.JSON_METADATA_TOKEN
+import com.stevesoltys.seedvault.service.metadata.JSON_METADATA_VERSION
+import com.stevesoltys.seedvault.service.metadata.JSON_PACKAGE_BACKUP_TYPE
+import com.stevesoltys.seedvault.service.metadata.JSON_PACKAGE_INSTALLER
+import com.stevesoltys.seedvault.service.metadata.JSON_PACKAGE_SHA256
+import com.stevesoltys.seedvault.service.metadata.JSON_PACKAGE_SIGNATURES
+import com.stevesoltys.seedvault.service.metadata.JSON_PACKAGE_STATE
+import com.stevesoltys.seedvault.service.metadata.JSON_PACKAGE_TIME
+import com.stevesoltys.seedvault.service.metadata.JSON_PACKAGE_VERSION
+import com.stevesoltys.seedvault.service.metadata.PackageState.QUOTA_EXCEEDED
+import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
+import com.stevesoltys.seedvault.service.metadata.METADATA_SALT_SIZE
+import com.stevesoltys.seedvault.service.metadata.MetadataReaderImpl
+import com.stevesoltys.seedvault.service.metadata.MetadataWriterImpl
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.metadata.PackageMetadataMap
 import io.mockk.mockk
 import org.json.JSONArray
 import org.json.JSONObject
@@ -22,10 +40,10 @@ import kotlin.random.Random
 @TestInstance(PER_CLASS)
 class MetadataReaderTest {
 
-    private val crypto = mockk<Crypto>()
+    private val cryptoService = mockk<CryptoService>()
 
-    private val encoder = MetadataWriterImpl(crypto)
-    private val decoder = MetadataReaderImpl(crypto)
+    private val encoder = MetadataWriterImpl(cryptoService)
+    private val decoder = MetadataReaderImpl(cryptoService)
 
     private val metadata = getMetadata()
     private val metadataByteArray = encoder.encode(metadata)
diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataServiceTest.kt
similarity index 85%
rename from app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt
rename to app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataServiceTest.kt
index 521aac65..295e0295 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataServiceTest.kt
@@ -7,19 +7,28 @@ import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
 import android.content.pm.ApplicationInfo.FLAG_SYSTEM
 import android.content.pm.PackageInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.stevesoltys.seedvault.Clock
+import com.stevesoltys.seedvault.util.TimeSource
 import com.stevesoltys.seedvault.TestApp
-import com.stevesoltys.seedvault.crypto.Crypto
-import com.stevesoltys.seedvault.encodeBase64
+import com.stevesoltys.seedvault.service.crypto.CryptoService
+import com.stevesoltys.seedvault.util.encodeBase64
 import com.stevesoltys.seedvault.getRandomByteArray
 import com.stevesoltys.seedvault.getRandomString
-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.QUOTA_EXCEEDED
-import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
-import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
-import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.service.metadata.BackupMetadata
+import com.stevesoltys.seedvault.service.metadata.BackupType
+import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
+import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.QUOTA_EXCEEDED
+import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
+import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
+import com.stevesoltys.seedvault.service.metadata.METADATA_CACHE_FILE
+import com.stevesoltys.seedvault.service.metadata.METADATA_SALT_SIZE
+import com.stevesoltys.seedvault.service.metadata.MetadataReader
+import com.stevesoltys.seedvault.service.metadata.MetadataService
+import com.stevesoltys.seedvault.service.metadata.MetadataWriter
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.metadata.PackageMetadataMap
+import com.stevesoltys.seedvault.service.settings.SettingsService
 import io.mockk.Runs
 import io.mockk.every
 import io.mockk.just
@@ -46,29 +55,29 @@ import kotlin.random.Random
     sdk = [33], // robolectric does not support 34, yet
     application = TestApp::class
 )
-class MetadataManagerTest {
+class MetadataServiceTest {
 
     private val context: Context = mockk()
-    private val clock: Clock = mockk()
-    private val crypto: Crypto = mockk()
+    private val timeSource: TimeSource = mockk()
+    private val cryptoService: CryptoService = mockk()
     private val metadataWriter: MetadataWriter = mockk()
     private val metadataReader: MetadataReader = mockk()
-    private val settingsManager: SettingsManager = mockk()
+    private val settingsService: SettingsService = mockk()
 
-    private val manager = MetadataManager(
+    private val manager = MetadataService(
         context = context,
-        clock = clock,
-        crypto = crypto,
+        timeSource = timeSource,
+        cryptoService = cryptoService,
         metadataWriter = metadataWriter,
         metadataReader = metadataReader,
-        settingsManager = settingsManager
+        settingsService = settingsService
     )
 
     private val time = 42L
     private val token = Random.nextLong()
     private val packageName = getRandomString()
     private val packageInfo = PackageInfo().apply {
-        packageName = this@MetadataManagerTest.packageName
+        packageName = this@MetadataServiceTest.packageName
         applicationInfo = ApplicationInfo().apply { flags = FLAG_ALLOW_BACKUP }
     }
     private val saltBytes = Random.nextBytes(METADATA_SALT_SIZE)
@@ -81,7 +90,7 @@ class MetadataManagerTest {
 
     @Before
     fun beforeEachTest() {
-        every { settingsManager.d2dBackupsEnabled() } returns false
+        every { settingsService.d2dBackupsEnabled() } returns false
     }
 
     @After
@@ -91,8 +100,8 @@ class MetadataManagerTest {
 
     @Test
     fun `test onDeviceInitialization()`() {
-        every { clock.time() } returns time
-        every { crypto.getRandomBytes(METADATA_SALT_SIZE) } returns saltBytes
+        every { timeSource.time() } returns time
+        every { cryptoService.getRandomBytes(METADATA_SALT_SIZE) } returns saltBytes
         expectReadFromCache()
         expectModifyMetadata(initialMetadata)
 
@@ -251,7 +260,7 @@ class MetadataManagerTest {
         updatedMetadata.packageMetadataMap[packageName] = packageMetadata
 
         expectReadFromCache()
-        every { clock.time() } returns time
+        every { timeSource.time() } returns time
         expectModifyMetadata(initialMetadata)
 
         manager.onPackageBackedUp(packageInfo, BackupType.FULL, storageOutputStream)
@@ -279,7 +288,7 @@ class MetadataManagerTest {
             PackageMetadata(updateTime, APK_AND_DATA, BackupType.KV)
 
         expectReadFromCache()
-        every { clock.time() } returns updateTime
+        every { timeSource.time() } returns updateTime
         every { metadataWriter.write(updatedMetadata, storageOutputStream) } throws IOException()
 
         try {
@@ -313,7 +322,7 @@ class MetadataManagerTest {
             PackageMetadata(time, state = APK_AND_DATA)
 
         expectReadFromCache()
-        every { clock.time() } returns time
+        every { timeSource.time() } returns time
         expectModifyMetadata(updatedMetadata)
 
         manager.onPackageBackedUp(packageInfo, BackupType.FULL, storageOutputStream)
diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataV0ReadTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataV0ReadTest.kt
index d0e11aca..ee8bd43f 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataV0ReadTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataV0ReadTest.kt
@@ -1,12 +1,15 @@
 package com.stevesoltys.seedvault.metadata
 
-import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
-import com.stevesoltys.seedvault.crypto.CryptoImpl
-import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES
+import com.stevesoltys.seedvault.service.crypto.CipherFactoryImpl
+import com.stevesoltys.seedvault.service.crypto.CryptoServiceImpl
+import com.stevesoltys.seedvault.service.crypto.KEY_SIZE_BYTES
 import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
-import com.stevesoltys.seedvault.header.HeaderReaderImpl
-import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
-import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
+import com.stevesoltys.seedvault.service.header.HeaderDecodeServiceImpl
+import com.stevesoltys.seedvault.service.metadata.BackupMetadata
+import com.stevesoltys.seedvault.service.metadata.MetadataReaderImpl
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
 import com.stevesoltys.seedvault.toByteArrayFromHex
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Test
@@ -26,10 +29,10 @@ internal class MetadataV0ReadTest {
     )
     private val keyManager = KeyManagerTestImpl(secretKey)
     private val cipherFactory = CipherFactoryImpl(keyManager)
-    private val headerReader = HeaderReaderImpl()
-    private val cryptoImpl = CryptoImpl(keyManager, cipherFactory, headerReader)
+    private val headerReader = HeaderDecodeServiceImpl()
+    private val cryptoServiceImpl = CryptoServiceImpl(keyManager, cipherFactory, headerReader)
 
-    private val reader = MetadataReaderImpl(cryptoImpl)
+    private val reader = MetadataReaderImpl(cryptoServiceImpl)
 
     private val packages = HashMap<String, PackageMetadata>().apply {
         put("org.example", PackageMetadata(23L, APK_AND_DATA))
diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt
index 4712551a..0e74de30 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt
@@ -1,13 +1,19 @@
 package com.stevesoltys.seedvault.metadata
 
-import com.stevesoltys.seedvault.crypto.Crypto
+import com.stevesoltys.seedvault.service.crypto.CryptoService
 import com.stevesoltys.seedvault.getRandomBase64
 import com.stevesoltys.seedvault.getRandomString
-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.QUOTA_EXCEEDED
-import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
+import com.stevesoltys.seedvault.service.metadata.ApkSplit
+import com.stevesoltys.seedvault.service.metadata.BackupMetadata
+import com.stevesoltys.seedvault.service.metadata.BackupType
+import com.stevesoltys.seedvault.service.metadata.MetadataReaderImpl
+import com.stevesoltys.seedvault.service.metadata.MetadataWriterImpl
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.metadata.PackageState.APK_AND_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
+import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.QUOTA_EXCEEDED
+import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
 import io.mockk.mockk
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Test
@@ -18,10 +24,10 @@ import kotlin.random.Random
 @TestInstance(PER_CLASS)
 internal class MetadataWriterDecoderTest {
 
-    private val crypto = mockk<Crypto>()
+    private val cryptoService = mockk<CryptoService>()
 
-    private val encoder = MetadataWriterImpl(crypto)
-    private val decoder = MetadataReaderImpl(crypto)
+    private val encoder = MetadataWriterImpl(cryptoService)
+    private val decoder = MetadataReaderImpl(cryptoService)
 
     @Test
     fun `encoded metadata matches decoded metadata (no packages)`() {
diff --git a/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/DocumentFileTest.kt b/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/DocumentFileTest.kt
index 3fd6491b..7dc1e6ea 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/DocumentFileTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/DocumentFileTest.kt
@@ -7,6 +7,7 @@ import android.provider.DocumentsContract
 import androidx.documentfile.provider.DocumentFile
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.stevesoltys.seedvault.TestApp
+import com.stevesoltys.seedvault.service.storage.saf.getTreeDocumentFile
 import io.mockk.every
 import io.mockk.mockk
 import org.junit.After
diff --git a/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/StoragePluginTest.kt b/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/StoragePluginTest.kt
index d06004fd..0b3a23a0 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/StoragePluginTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/StoragePluginTest.kt
@@ -1,6 +1,9 @@
 package com.stevesoltys.seedvault.plugins.saf
 
 import androidx.documentfile.provider.DocumentFile
+import com.stevesoltys.seedvault.service.storage.saf.DocumentsProviderStoragePlugin
+import com.stevesoltys.seedvault.service.storage.saf.DocumentsStorage
+import com.stevesoltys.seedvault.service.storage.saf.listFilesBlocking
 import com.stevesoltys.seedvault.transport.backup.BackupTest
 import io.mockk.Runs
 import io.mockk.coEvery
@@ -38,7 +41,7 @@ internal class StoragePluginTest : BackupTest() {
     fun `test initializeDevice`() = runBlocking {
         // get current set dir and for that the current token
         every { storage getProperty "currentToken" } returns token
-        every { settingsManager.getToken() } returns token
+        every { settingsService.getToken() } returns token
         every { storage getProperty "storage" } returns null // just to check if isUsb
         coEvery { storage.getSetDir(token) } returns setDir
         // delete contents of current set dir
diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupServiceRestoreTest.kt
similarity index 77%
rename from app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt
rename to app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupServiceRestoreTest.kt
index f8805c78..4ec66a24 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupServiceRestoreTest.kt
@@ -7,15 +7,21 @@ import android.graphics.drawable.Drawable
 import android.util.PackageUtils
 import com.stevesoltys.seedvault.assertReadEquals
 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.service.app.backup.apk.ApkBackupService
+import com.stevesoltys.seedvault.service.metadata.ApkSplit
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.metadata.PackageMetadataMap
+import com.stevesoltys.seedvault.service.metadata.PackageState
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
 import com.stevesoltys.seedvault.transport.TransportTest
-import com.stevesoltys.seedvault.transport.backup.ApkBackup
+import com.stevesoltys.seedvault.ui.restore.RestorableBackup
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallResult
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstaller
+import com.stevesoltys.seedvault.ui.restore.apk.ApkRestore
+import com.stevesoltys.seedvault.ui.restore.apk.ApkSplitCompatibilityChecker
+import com.stevesoltys.seedvault.ui.restore.apk.MutableInstallResult
 import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.mockk
@@ -40,7 +46,7 @@ import kotlin.random.Random
 
 @ExperimentalCoroutinesApi
 @Suppress("BlockingMethodInNonBlockingContext")
-internal class ApkBackupRestoreTest : TransportTest() {
+internal class ApkBackupServiceRestoreTest : TransportTest() {
 
     private val pm: PackageManager = mockk()
     private val strictContext: Context = mockk<Context>().apply {
@@ -53,12 +59,13 @@ internal class ApkBackupRestoreTest : TransportTest() {
     private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
     private val apkInstaller: ApkInstaller = mockk()
 
-    private val apkBackup = ApkBackup(pm, crypto, settingsManager, metadataManager)
+    private val apkBackupService =
+        ApkBackupService(pm, cryptoService, settingsService, metadataService)
     private val apkRestore: ApkRestore = ApkRestore(
         context = strictContext,
         storagePlugin = storagePlugin,
         legacyStoragePlugin = legacyStoragePlugin,
-        crypto = crypto,
+        cryptoService = cryptoService,
         splitCompatChecker = splitCompatChecker,
         apkInstaller = apkInstaller
     )
@@ -107,20 +114,24 @@ internal class ApkBackupRestoreTest : TransportTest() {
             writeBytes(splitBytes)
         }.absolutePath)
 
-        every { settingsManager.backupApks() } returns true
+        every { settingsService.backupApks() } returns true
         every { sigInfo.hasMultipleSigners() } returns false
         every { sigInfo.signingCertificateHistory } returns sigs
         every { PackageUtils.computeSha256DigestBytes(signatureBytes) } returns signatureHash
         every {
-            metadataManager.getPackageMetadata(packageInfo.packageName)
+            metadataService.getPackageMetadata(packageInfo.packageName)
         } returns packageMetadata
         every { pm.getInstallSourceInfo(packageInfo.packageName) } returns mockk(relaxed = true)
-        every { metadataManager.salt } returns salt
-        every { crypto.getNameForApk(salt, packageName) } returns name
-        every { crypto.getNameForApk(salt, packageName, splitName) } returns suffixName
+        every { metadataService.salt } returns salt
+        every { cryptoService.getNameForApk(salt, packageName) } returns name
+        every { cryptoService.getNameForApk(salt, packageName, splitName) } returns suffixName
         every { storagePlugin.providerPackageName } returns storageProviderPackageName
 
-        apkBackup.backupApkIfNecessary(packageInfo, PackageState.APK_AND_DATA, outputStreamGetter)
+        apkBackupService.backupApkIfNecessary(
+            packageInfo,
+            PackageState.APK_AND_DATA,
+            outputStreamGetter
+        )
 
         assertArrayEquals(apkBytes, outputStream.toByteArray())
         assertArrayEquals(splitBytes, splitOutputStream.toByteArray())
@@ -131,7 +142,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
         val cacheFiles = slot<List<File>>()
 
         every { strictContext.cacheDir } returns tmpFile
-        every { crypto.getNameForApk(salt, packageName, "") } returns name
+        every { cryptoService.getNameForApk(salt, packageName, "") } returns name
         coEvery { storagePlugin.getInputStream(token, name) } returns inputStream
         every { pm.getPackageArchiveInfo(capture(apkPath), any<Int>()) } returns packageInfo
         every { applicationInfo.loadIcon(pm) } returns icon
@@ -139,7 +150,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
         every {
             splitCompatChecker.isCompatible(metadata.deviceName, listOf(splitName))
         } returns true
-        every { crypto.getNameForApk(salt, packageName, splitName) } returns suffixName
+        every { cryptoService.getNameForApk(salt, packageName, splitName) } returns suffixName
         coEvery { storagePlugin.getInputStream(token, suffixName) } returns splitInputStream
         coEvery {
             apkInstaller.install(capture(cacheFiles), packageName, installerName, any())
diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt
index 6eb1e969..4504be43 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt
@@ -10,18 +10,24 @@ import android.graphics.drawable.Drawable
 import com.stevesoltys.seedvault.getRandomBase64
 import com.stevesoltys.seedvault.getRandomByteArray
 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.plugins.LegacyStoragePlugin
-import com.stevesoltys.seedvault.plugins.StoragePlugin
-import com.stevesoltys.seedvault.restore.RestorableBackup
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
-import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
+import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.metadata.ApkSplit
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.metadata.PackageMetadataMap
 import com.stevesoltys.seedvault.transport.TransportTest
+import com.stevesoltys.seedvault.ui.restore.RestorableBackup
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallResult
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.FAILED
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.FAILED_SYSTEM_APP
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.IN_PROGRESS
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.QUEUED
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstallState.SUCCEEDED
+import com.stevesoltys.seedvault.ui.restore.apk.ApkInstaller
+import com.stevesoltys.seedvault.ui.restore.apk.ApkRestore
+import com.stevesoltys.seedvault.ui.restore.apk.ApkSplitCompatibilityChecker
+import com.stevesoltys.seedvault.ui.restore.apk.InstallResult
+import com.stevesoltys.seedvault.ui.restore.apk.MutableInstallResult
 import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.mockk
@@ -58,7 +64,7 @@ internal class ApkRestoreTest : TransportTest() {
         strictContext,
         storagePlugin,
         legacyStoragePlugin,
-        crypto,
+        cryptoService,
         splitCompatChecker,
         apkInstaller
     )
@@ -94,7 +100,7 @@ internal class ApkRestoreTest : TransportTest() {
         val backup = swapPackages(hashMapOf(packageName to packageMetadata))
 
         every { strictContext.cacheDir } returns File(tmpDir.toString())
-        every { crypto.getNameForApk(salt, packageName, "") } returns name
+        every { cryptoService.getNameForApk(salt, packageName, "") } returns name
         coEvery { storagePlugin.getInputStream(token, name) } returns apkInputStream
         every { storagePlugin.providerPackageName } returns storageProviderPackageName
 
@@ -109,7 +115,7 @@ internal class ApkRestoreTest : TransportTest() {
         packageInfo.packageName = getRandomString()
 
         every { strictContext.cacheDir } returns File(tmpDir.toString())
-        every { crypto.getNameForApk(salt, packageName, "") } returns name
+        every { cryptoService.getNameForApk(salt, packageName, "") } returns name
         coEvery { storagePlugin.getInputStream(token, name) } returns apkInputStream
         every { pm.getPackageArchiveInfo(any(), any<Int>()) } returns packageInfo
         every { storagePlugin.providerPackageName } returns storageProviderPackageName
@@ -297,7 +303,7 @@ internal class ApkRestoreTest : TransportTest() {
         cacheBaseApkAndGetInfo(tmpDir)
 
         every { splitCompatChecker.isCompatible(deviceName, listOf(splitName)) } returns true
-        every { crypto.getNameForApk(salt, packageName, splitName) } returns suffixName
+        every { cryptoService.getNameForApk(salt, packageName, splitName) } returns suffixName
         coEvery {
             storagePlugin.getInputStream(token, suffixName)
         } returns ByteArrayInputStream(getRandomByteArray())
@@ -322,7 +328,7 @@ internal class ApkRestoreTest : TransportTest() {
             cacheBaseApkAndGetInfo(tmpDir)
 
             every { splitCompatChecker.isCompatible(deviceName, listOf(splitName)) } returns true
-            every { crypto.getNameForApk(salt, packageName, splitName) } returns suffixName
+            every { cryptoService.getNameForApk(salt, packageName, splitName) } returns suffixName
             coEvery { storagePlugin.getInputStream(token, suffixName) } throws IOException()
             every { storagePlugin.providerPackageName } returns storageProviderPackageName
 
@@ -359,9 +365,9 @@ internal class ApkRestoreTest : TransportTest() {
         val split2InputStream = ByteArrayInputStream(split2Bytes)
         val suffixName1 = getRandomString()
         val suffixName2 = getRandomString()
-        every { crypto.getNameForApk(salt, packageName, split1Name) } returns suffixName1
+        every { cryptoService.getNameForApk(salt, packageName, split1Name) } returns suffixName1
         coEvery { storagePlugin.getInputStream(token, suffixName1) } returns split1InputStream
-        every { crypto.getNameForApk(salt, packageName, split2Name) } returns suffixName2
+        every { cryptoService.getNameForApk(salt, packageName, split2Name) } returns suffixName2
         coEvery { storagePlugin.getInputStream(token, suffixName2) } returns split2InputStream
         every { storagePlugin.providerPackageName } returns storageProviderPackageName
 
@@ -410,7 +416,7 @@ internal class ApkRestoreTest : TransportTest() {
 
     private fun cacheBaseApkAndGetInfo(tmpDir: Path) {
         every { strictContext.cacheDir } returns File(tmpDir.toString())
-        every { crypto.getNameForApk(salt, packageName, "") } returns name
+        every { cryptoService.getNameForApk(salt, packageName, "") } returns name
         coEvery { storagePlugin.getInputStream(token, name) } returns apkInputStream
         every { pm.getPackageArchiveInfo(any(), any<Int>()) } returns packageInfo
         every { applicationInfo.loadIcon(pm) } returns icon
diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkSplitCompatibilityCheckerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkSplitCompatibilityCheckerTest.kt
index 6d10afb8..1a3cd74a 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkSplitCompatibilityCheckerTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkSplitCompatibilityCheckerTest.kt
@@ -2,6 +2,8 @@ package com.stevesoltys.seedvault.restore.install
 
 import com.stevesoltys.seedvault.getRandomString
 import com.stevesoltys.seedvault.transport.TransportTest
+import com.stevesoltys.seedvault.ui.restore.apk.ApkSplitCompatibilityChecker
+import com.stevesoltys.seedvault.ui.restore.apk.DeviceInfo
 import io.mockk.every
 import io.mockk.mockk
 import org.junit.Assert.assertEquals
diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/DeviceInfoTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/DeviceInfoTest.kt
index 2ee5f7c2..baab1ea0 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/DeviceInfoTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/DeviceInfoTest.kt
@@ -8,6 +8,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.stevesoltys.seedvault.R
 import com.stevesoltys.seedvault.TestApp
 import com.stevesoltys.seedvault.getRandomString
+import com.stevesoltys.seedvault.ui.restore.apk.DeviceInfo
 import io.mockk.every
 import io.mockk.mockk
 import org.junit.After
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 824230e8..574218f8 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
@@ -7,29 +7,29 @@ import android.app.backup.BackupTransport.TRANSPORT_OK
 import android.app.backup.RestoreDescription
 import android.app.backup.RestoreDescription.TYPE_FULL_STREAM
 import android.os.ParcelFileDescriptor
-import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
-import com.stevesoltys.seedvault.crypto.CryptoImpl
+import com.stevesoltys.seedvault.service.crypto.CipherFactoryImpl
+import com.stevesoltys.seedvault.service.crypto.CryptoServiceImpl
 import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
-import com.stevesoltys.seedvault.header.HeaderReaderImpl
-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
-import com.stevesoltys.seedvault.transport.backup.KVBackup
-import com.stevesoltys.seedvault.transport.backup.PackageService
+import com.stevesoltys.seedvault.service.header.HeaderDecodeServiceImpl
+import com.stevesoltys.seedvault.service.header.MAX_SEGMENT_CLEARTEXT_LENGTH
+import com.stevesoltys.seedvault.service.metadata.BackupType
+import com.stevesoltys.seedvault.service.metadata.MetadataReaderImpl
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
+import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.storage.saf.FILE_BACKUP_METADATA
+import com.stevesoltys.seedvault.service.app.backup.apk.ApkBackupService
+import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
+import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
+import com.stevesoltys.seedvault.service.app.backup.InputFactory
+import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
+import com.stevesoltys.seedvault.service.app.PackageService
 import com.stevesoltys.seedvault.transport.backup.TestKvDbManager
-import com.stevesoltys.seedvault.transport.restore.FullRestore
-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.service.app.restore.full.FullRestore
+import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
+import com.stevesoltys.seedvault.service.app.restore.OutputFactory
+import com.stevesoltys.seedvault.service.app.restore.coordinator.RestoreCoordinator
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import io.mockk.CapturingSlot
 import io.mockk.Runs
@@ -54,30 +54,31 @@ internal class CoordinatorIntegrationTest : TransportTest() {
     private val outputFactory = mockk<OutputFactory>()
     private val keyManager = KeyManagerTestImpl()
     private val cipherFactory = CipherFactoryImpl(keyManager)
-    private val headerReader = HeaderReaderImpl()
-    private val cryptoImpl = CryptoImpl(keyManager, cipherFactory, headerReader)
-    private val metadataReader = MetadataReaderImpl(cryptoImpl)
+    private val headerReader = HeaderDecodeServiceImpl()
+    private val cryptoServiceImpl = CryptoServiceImpl(keyManager, cipherFactory, headerReader)
+    private val metadataReader = MetadataReaderImpl(cryptoServiceImpl)
     private val notificationManager = mockk<BackupNotificationManager>()
     private val dbManager = TestKvDbManager()
 
     @Suppress("Deprecation")
     private val legacyPlugin = mockk<LegacyStoragePlugin>()
     private val backupPlugin = mockk<StoragePlugin>()
-    private val kvBackup =
-        KVBackup(backupPlugin, settingsManager, inputFactory, cryptoImpl, dbManager)
-    private val fullBackup = FullBackup(backupPlugin, settingsManager, inputFactory, cryptoImpl)
-    private val apkBackup = mockk<ApkBackup>()
+    private val kvBackupService =
+        KVBackupService(backupPlugin, settingsService, inputFactory, cryptoServiceImpl, dbManager)
+    private val fullBackupService =
+        FullBackupService(backupPlugin, settingsService, inputFactory, cryptoServiceImpl)
+    private val apkBackupService = mockk<ApkBackupService>()
     private val packageService: PackageService = mockk()
-    private val backup = BackupCoordinator(
+    private val backup = BackupCoordinatorService(
         context,
         backupPlugin,
-        kvBackup,
-        fullBackup,
-        apkBackup,
-        clock,
+        kvBackupService,
+        fullBackupService,
+        apkBackupService,
+        timeSource,
         packageService,
-        metadataManager,
-        settingsManager,
+        metadataService,
+        settingsService,
         notificationManager
     )
 
@@ -86,16 +87,16 @@ internal class CoordinatorIntegrationTest : TransportTest() {
         legacyPlugin,
         outputFactory,
         headerReader,
-        cryptoImpl,
+        cryptoServiceImpl,
         dbManager
     )
     private val fullRestore =
-        FullRestore(backupPlugin, legacyPlugin, outputFactory, headerReader, cryptoImpl)
+        FullRestore(backupPlugin, legacyPlugin, outputFactory, headerReader, cryptoServiceImpl)
     private val restore = RestoreCoordinator(
         context,
-        crypto,
-        settingsManager,
-        metadataManager,
+        cryptoService,
+        settingsService,
+        metadataService,
         notificationManager,
         backupPlugin,
         kvRestore,
@@ -113,7 +114,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
     private val key2 = "RestoreKey2"
 
     // as we use real crypto, we need a real name for packageInfo
-    private val realName = cryptoImpl.getNameForPackage(salt, packageInfo.packageName)
+    private val realName = cryptoServiceImpl.getNameForPackage(salt, packageInfo.packageName)
 
     @Test
     fun `test key-value backup and restore with 2 records`() = runBlocking {
@@ -121,9 +122,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
         val value2 = CapturingSlot<ByteArray>()
         val bOutputStream = ByteArrayOutputStream()
 
-        every { metadataManager.requiresInit } returns false
-        every { settingsManager.getToken() } returns token
-        every { metadataManager.salt } returns salt
+        every { metadataService.requiresInit } returns false
+        every { settingsService.getToken() } returns token
+        every { metadataService.salt } returns salt
         // read one key/value record and write it to output stream
         every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput
         every { backupDataInput.readNextHeader() } returns true andThen true andThen false
@@ -138,16 +139,16 @@ internal class CoordinatorIntegrationTest : TransportTest() {
             appData2.size
         }
         coEvery {
-            apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any())
+            apkBackupService.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any())
         } returns packageMetadata
         coEvery {
             backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA)
         } returns metadataOutputStream
         every {
-            metadataManager.onApkBackedUp(packageInfo, packageMetadata, metadataOutputStream)
+            metadataService.onApkBackedUp(packageInfo, packageMetadata, metadataOutputStream)
         } just Runs
         every {
-            metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, metadataOutputStream)
+            metadataService.onPackageBackedUp(packageInfo, BackupType.KV, metadataOutputStream)
         } just Runs
 
         // start K/V backup
@@ -164,7 +165,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
         assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo)))
 
         // find data for K/V backup
-        every { crypto.getNameForPackage(metadata.salt, packageInfo.packageName) } returns name
+        every {
+            cryptoService.getNameForPackage(metadata.salt, packageInfo.packageName)
+        } returns name
         coEvery { backupPlugin.hasData(token, name) } returns true
 
         val restoreDescription = restore.nextRestorePackage() ?: fail()
@@ -198,9 +201,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
         val appData = ByteArray(size).apply { Random.nextBytes(this) }
         val bOutputStream = ByteArrayOutputStream()
 
-        every { metadataManager.requiresInit } returns false
-        every { settingsManager.getToken() } returns token
-        every { metadataManager.salt } returns salt
+        every { metadataService.requiresInit } returns false
+        every { settingsService.getToken() } returns token
+        every { metadataService.salt } returns salt
         // read one key/value record and write it to output stream
         every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput
         every { backupDataInput.readNextHeader() } returns true andThen false
@@ -210,13 +213,19 @@ 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
-        every { settingsManager.getToken() } returns token
+        coEvery {
+            apkBackupService.backupApkIfNecessary(
+                packageInfo,
+                UNKNOWN_ERROR,
+                any()
+            )
+        } returns null
+        every { settingsService.getToken() } returns token
         coEvery {
             backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA)
         } returns metadataOutputStream
         every {
-            metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, metadataOutputStream)
+            metadataService.onPackageBackedUp(packageInfo, BackupType.KV, metadataOutputStream)
         } just Runs
 
         // start K/V backup
@@ -233,7 +242,12 @@ internal class CoordinatorIntegrationTest : TransportTest() {
         assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo)))
 
         // find data for K/V backup
-        every { crypto.getNameForPackage(metadata.salt, packageInfo.packageName) } returns name
+        every {
+            cryptoService.getNameForPackage(
+                metadata.salt,
+                packageInfo.packageName
+            )
+        } returns name
         coEvery { backupPlugin.hasData(token, name) } returns true
 
         val restoreDescription = restore.nextRestorePackage() ?: fail()
@@ -268,28 +282,28 @@ internal class CoordinatorIntegrationTest : TransportTest() {
         val bInputStream = ByteArrayInputStream(appData)
         coEvery { backupPlugin.getOutputStream(token, realName) } returns bOutputStream
         every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
-        every { settingsManager.isQuotaUnlimited() } returns false
+        every { settingsService.isQuotaUnlimited() } returns false
         coEvery {
-            apkBackup.backupApkIfNecessary(
+            apkBackupService.backupApkIfNecessary(
                 packageInfo,
                 UNKNOWN_ERROR,
                 any()
             )
         } returns packageMetadata
-        every { settingsManager.getToken() } returns token
-        every { metadataManager.salt } returns salt
+        every { settingsService.getToken() } returns token
+        every { metadataService.salt } returns salt
         coEvery {
             backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA)
         } returns metadataOutputStream
         every {
-            metadataManager.onApkBackedUp(
+            metadataService.onApkBackedUp(
                 packageInfo,
                 packageMetadata,
                 metadataOutputStream
             )
         } just Runs
         every {
-            metadataManager.onPackageBackedUp(packageInfo, BackupType.FULL, metadataOutputStream)
+            metadataService.onPackageBackedUp(packageInfo, BackupType.FULL, metadataOutputStream)
         } just Runs
 
         // perform backup to output stream
@@ -303,7 +317,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
         assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo)))
 
         // finds data for full backup
-        every { crypto.getNameForPackage(salt, packageInfo.packageName) } returns name
+        every { cryptoService.getNameForPackage(salt, packageInfo.packageName) } returns name
         coEvery { backupPlugin.hasData(token, name) } returns true
 
         val restoreDescription = restore.nextRestorePackage() ?: 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 0af1caa2..50d97e1c 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt
@@ -7,18 +7,18 @@ import android.content.pm.ApplicationInfo.FLAG_INSTALLED
 import android.content.pm.PackageInfo
 import android.content.pm.SigningInfo
 import android.util.Log
-import com.stevesoltys.seedvault.Clock
+import com.stevesoltys.seedvault.util.TimeSource
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
-import com.stevesoltys.seedvault.crypto.Crypto
+import com.stevesoltys.seedvault.service.crypto.CryptoService
 import com.stevesoltys.seedvault.getRandomBase64
 import com.stevesoltys.seedvault.getRandomString
-import com.stevesoltys.seedvault.metadata.BackupMetadata
-import com.stevesoltys.seedvault.metadata.BackupType
-import com.stevesoltys.seedvault.metadata.METADATA_SALT_SIZE
-import com.stevesoltys.seedvault.metadata.MetadataManager
-import com.stevesoltys.seedvault.metadata.PackageMetadata
-import com.stevesoltys.seedvault.metadata.PackageMetadataMap
-import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.service.metadata.BackupMetadata
+import com.stevesoltys.seedvault.service.metadata.BackupType
+import com.stevesoltys.seedvault.service.metadata.METADATA_SALT_SIZE
+import com.stevesoltys.seedvault.service.metadata.MetadataService
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.metadata.PackageMetadataMap
+import com.stevesoltys.seedvault.service.settings.SettingsService
 import io.mockk.every
 import io.mockk.mockk
 import io.mockk.mockkStatic
@@ -29,10 +29,10 @@ import kotlin.random.Random
 @TestInstance(PER_METHOD)
 internal abstract class TransportTest {
 
-    protected val clock: Clock = mockk()
-    protected val crypto = mockk<Crypto>()
-    protected val settingsManager = mockk<SettingsManager>()
-    protected val metadataManager = mockk<MetadataManager>()
+    protected val timeSource: TimeSource = mockk()
+    protected val cryptoService = mockk<CryptoService>()
+    protected val settingsService = mockk<SettingsService>()
+    protected val metadataService = mockk<MetadataService>()
     protected val context = mockk<Context>(relaxed = true)
 
     protected val sigInfo: SigningInfo = mockk()
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupServiceTest.kt
similarity index 76%
rename from app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupTest.kt
rename to app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupServiceTest.kt
index 2137e565..3f1ede35 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupServiceTest.kt
@@ -10,9 +10,10 @@ import android.content.pm.Signature
 import android.util.PackageUtils
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
 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.service.metadata.ApkSplit
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
+import com.stevesoltys.seedvault.service.app.backup.apk.ApkBackupService
 import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.mockk
@@ -33,12 +34,13 @@ import java.nio.file.Path
 import kotlin.random.Random
 
 @Suppress("BlockingMethodInNonBlockingContext")
-internal class ApkBackupTest : BackupTest() {
+internal class ApkBackupServiceTest : BackupTest() {
 
     private val pm: PackageManager = mockk()
     private val streamGetter: suspend (name: String) -> OutputStream = mockk()
 
-    private val apkBackup = ApkBackup(pm, crypto, settingsManager, metadataManager)
+    private val apkBackupService =
+        ApkBackupService(pm, cryptoService, settingsService, metadataService)
 
     private val signatureBytes = byteArrayOf(0x01, 0x02, 0x03)
     private val signatureHash = byteArrayOf(0x03, 0x02, 0x01)
@@ -56,32 +58,32 @@ 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(apkBackupService.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
     }
 
     @Test
     fun `does not back up when setting disabled`() = runBlocking {
-        every { settingsManager.backupApks() } returns false
+        every { settingsService.backupApks() } returns false
 
-        assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
+        assertNull(apkBackupService.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
     }
 
     @Test
     fun `does not back up test-only apps`() = runBlocking {
         packageInfo.applicationInfo.flags = FLAG_TEST_ONLY
 
-        every { settingsManager.backupApks() } returns true
+        every { settingsService.backupApks() } returns true
 
-        assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
+        assertNull(apkBackupService.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
     }
 
     @Test
     fun `does not back up system apps`() = runBlocking {
         packageInfo.applicationInfo.flags = FLAG_SYSTEM
 
-        every { settingsManager.backupApks() } returns true
+        every { settingsService.backupApks() } returns true
 
-        assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
+        assertNull(apkBackupService.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
     }
 
     @Test
@@ -93,7 +95,7 @@ internal class ApkBackupTest : BackupTest() {
 
         expectChecks(packageMetadata)
 
-        assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
+        assertNull(apkBackupService.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
     }
 
     @Test
@@ -104,21 +106,27 @@ internal class ApkBackupTest : BackupTest() {
 
         assertThrows(IOException::class.java) {
             runBlocking {
-                assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
+                assertNull(
+                    apkBackupService.backupApkIfNecessary(
+                        packageInfo,
+                        UNKNOWN_ERROR,
+                        streamGetter
+                    )
+                )
             }
         }
     }
 
     @Test
     fun `do not accept empty signature`() = runBlocking {
-        every { settingsManager.backupApks() } returns true
+        every { settingsService.backupApks() } returns true
         every {
-            metadataManager.getPackageMetadata(packageInfo.packageName)
+            metadataService.getPackageMetadata(packageInfo.packageName)
         } returns packageMetadata
         every { sigInfo.hasMultipleSigners() } returns false
         every { sigInfo.signingCertificateHistory } returns emptyArray()
 
-        assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
+        assertNull(apkBackupService.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
     }
 
     @Test
@@ -140,8 +148,8 @@ internal class ApkBackupTest : BackupTest() {
         )
 
         expectChecks()
-        every { metadataManager.salt } returns salt
-        every { crypto.getNameForApk(salt, packageInfo.packageName) } returns name
+        every { metadataService.salt } returns salt
+        every { cryptoService.getNameForApk(salt, packageInfo.packageName) } returns name
         coEvery { streamGetter.invoke(name) } returns apkOutputStream
         every {
             pm.getInstallSourceInfo(packageInfo.packageName)
@@ -149,7 +157,7 @@ internal class ApkBackupTest : BackupTest() {
 
         assertEquals(
             updatedMetadata,
-            apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)
+            apkBackupService.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)
         )
         assertArrayEquals(apkBytes, apkOutputStream.toByteArray())
     }
@@ -203,13 +211,13 @@ internal class ApkBackupTest : BackupTest() {
         val suffixName2 = getRandomString()
 
         expectChecks()
-        every { metadataManager.salt } returns salt
-        every { crypto.getNameForApk(salt, packageInfo.packageName) } returns name
+        every { metadataService.salt } returns salt
+        every { cryptoService.getNameForApk(salt, packageInfo.packageName) } returns name
         every {
-            crypto.getNameForApk(salt, packageInfo.packageName, split1Name)
+            cryptoService.getNameForApk(salt, packageInfo.packageName, split1Name)
         } returns suffixName1
         every {
-            crypto.getNameForApk(salt, packageInfo.packageName, split2Name)
+            cryptoService.getNameForApk(salt, packageInfo.packageName, split2Name)
         } returns suffixName2
         coEvery { streamGetter.invoke(name) } returns apkOutputStream
         coEvery { streamGetter.invoke(suffixName1) } returns split1OutputStream
@@ -221,7 +229,7 @@ internal class ApkBackupTest : BackupTest() {
 
         assertEquals(
             updatedMetadata,
-            apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)
+            apkBackupService.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)
         )
         assertArrayEquals(apkBytes, apkOutputStream.toByteArray())
         assertArrayEquals(split1Bytes, split1OutputStream.toByteArray())
@@ -229,9 +237,9 @@ internal class ApkBackupTest : BackupTest() {
     }
 
     private fun expectChecks(packageMetadata: PackageMetadata = this.packageMetadata) {
-        every { settingsManager.backupApks() } returns true
+        every { settingsService.backupApks() } returns true
         every {
-            metadataManager.getPackageMetadata(packageInfo.packageName)
+            metadataService.getPackageMetadata(packageInfo.packageName)
         } returns packageMetadata
         every { PackageUtils.computeSha256DigestBytes(signatureBytes) } returns signatureHash
         every { sigInfo.hasMultipleSigners() } returns false
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorServiceTest.kt
similarity index 74%
rename from app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt
rename to app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorServiceTest.kt
index bb9de194..96cb731d 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorServiceTest.kt
@@ -12,16 +12,22 @@ import android.os.ParcelFileDescriptor
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
 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.service.metadata.BackupType
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.metadata.PackageState.NOT_ALLOWED
+import com.stevesoltys.seedvault.service.metadata.PackageState.NO_DATA
+import com.stevesoltys.seedvault.service.metadata.PackageState.QUOTA_EXCEEDED
+import com.stevesoltys.seedvault.service.metadata.PackageState.UNKNOWN_ERROR
+import com.stevesoltys.seedvault.service.metadata.PackageState.WAS_STOPPED
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.storage.saf.FILE_BACKUP_METADATA
+import com.stevesoltys.seedvault.service.app.PackageService
+import com.stevesoltys.seedvault.service.app.backup.coordinator.BackupCoordinatorService
+import com.stevesoltys.seedvault.service.settings.Storage
+import com.stevesoltys.seedvault.service.app.backup.apk.ApkBackupService
+import com.stevesoltys.seedvault.service.app.backup.full.DEFAULT_QUOTA_FULL_BACKUP
+import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
+import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import io.mockk.Runs
 import io.mockk.coEvery
@@ -38,25 +44,25 @@ import java.io.OutputStream
 import kotlin.random.Random
 
 @Suppress("BlockingMethodInNonBlockingContext")
-internal class BackupCoordinatorTest : BackupTest() {
+internal class BackupCoordinatorServiceTest : BackupTest() {
 
     private val plugin = mockk<StoragePlugin>()
-    private val kv = mockk<KVBackup>()
-    private val full = mockk<FullBackup>()
-    private val apkBackup = mockk<ApkBackup>()
+    private val kv = mockk<KVBackupService>()
+    private val full = mockk<FullBackupService>()
+    private val apkBackupService = mockk<ApkBackupService>()
     private val packageService: PackageService = mockk()
     private val notificationManager = mockk<BackupNotificationManager>()
 
-    private val backup = BackupCoordinator(
+    private val backup = BackupCoordinatorService(
         context,
         plugin,
         kv,
         full,
-        apkBackup,
-        clock,
+        apkBackupService,
+        timeSource,
         packageService,
-        metadataManager,
-        settingsManager,
+        metadataService,
+        settingsService,
         notificationManager
     )
 
@@ -75,7 +81,7 @@ internal class BackupCoordinatorTest : BackupTest() {
         expectStartNewRestoreSet()
         coEvery { plugin.initializeDevice() } just Runs
         coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream
-        every { metadataManager.onDeviceInitialization(token, metadataOutputStream) } just Runs
+        every { metadataService.onDeviceInitialization(token, metadataOutputStream) } just Runs
         every { kv.hasState() } returns false
         every { full.hasState() } returns false
         every { metadataOutputStream.close() } just Runs
@@ -87,8 +93,8 @@ internal class BackupCoordinatorTest : BackupTest() {
     }
 
     private suspend fun expectStartNewRestoreSet() {
-        every { clock.time() } returns token
-        every { settingsManager.setNewToken(token) } just Runs
+        every { timeSource.time() } returns token
+        every { settingsService.setNewToken(token) } just Runs
         coEvery { plugin.startNewRestoreSet(token) } just Runs
     }
 
@@ -98,8 +104,8 @@ internal class BackupCoordinatorTest : BackupTest() {
 
         expectStartNewRestoreSet()
         coEvery { plugin.initializeDevice() } throws IOException()
-        every { metadataManager.requiresInit } returns maybeTrue
-        every { settingsManager.canDoBackupNow() } returns !maybeTrue
+        every { metadataService.requiresInit } returns maybeTrue
+        every { settingsService.canDoBackupNow() } returns !maybeTrue
         every { notificationManager.onBackupError() } just Runs
 
         assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
@@ -117,8 +123,8 @@ internal class BackupCoordinatorTest : BackupTest() {
         runBlocking {
             expectStartNewRestoreSet()
             coEvery { plugin.initializeDevice() } throws IOException()
-            every { metadataManager.requiresInit } returns false
-            every { settingsManager.canDoBackupNow() } returns false
+            every { metadataService.requiresInit } returns false
+            every { settingsService.canDoBackupNow() } returns false
 
             assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
 
@@ -134,12 +140,12 @@ internal class BackupCoordinatorTest : BackupTest() {
     fun `performIncrementalBackup of @pm@ causes re-init when legacy format`() = runBlocking {
         val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
 
-        every { settingsManager.canDoBackupNow() } returns true
-        every { metadataManager.requiresInit } returns true
+        every { settingsService.canDoBackupNow() } returns true
+        every { metadataService.requiresInit } returns true
 
         // start new restore set
-        every { clock.time() } returns token + 1
-        every { settingsManager.setNewToken(token + 1) } just Runs
+        every { timeSource.time() } returns token + 1
+        every { settingsService.setNewToken(token + 1) } just Runs
         coEvery { plugin.startNewRestoreSet(token + 1) } just Runs
 
         every { data.close() } just Runs
@@ -170,8 +176,8 @@ internal class BackupCoordinatorTest : BackupTest() {
 
     @Test
     fun `clearing KV backup data throws`() = runBlocking {
-        every { settingsManager.getToken() } returns token
-        every { metadataManager.salt } returns salt
+        every { settingsService.getToken() } returns token
+        every { metadataService.salt } returns salt
         coEvery { kv.clearBackupData(packageInfo, token, salt) } throws IOException()
 
         assertEquals(TRANSPORT_ERROR, backup.clearBackupData(packageInfo))
@@ -179,8 +185,8 @@ internal class BackupCoordinatorTest : BackupTest() {
 
     @Test
     fun `clearing full backup data throws`() = runBlocking {
-        every { settingsManager.getToken() } returns token
-        every { metadataManager.salt } returns salt
+        every { settingsService.getToken() } returns token
+        every { metadataService.salt } returns salt
         coEvery { kv.clearBackupData(packageInfo, token, salt) } just Runs
         coEvery { full.clearBackupData(packageInfo, token, salt) } throws IOException()
 
@@ -189,8 +195,8 @@ internal class BackupCoordinatorTest : BackupTest() {
 
     @Test
     fun `clearing backup data succeeds`() = runBlocking {
-        every { settingsManager.getToken() } returns token
-        every { metadataManager.salt } returns salt
+        every { settingsService.getToken() } returns token
+        every { metadataService.salt } returns salt
         coEvery { kv.clearBackupData(packageInfo, token, salt) } just Runs
         coEvery { full.clearBackupData(packageInfo, token, salt) } just Runs
 
@@ -208,10 +214,10 @@ internal class BackupCoordinatorTest : BackupTest() {
         every { full.hasState() } returns false
         every { kv.getCurrentPackage() } returns packageInfo
         coEvery { kv.finishBackup() } returns TRANSPORT_OK
-        every { settingsManager.getToken() } returns token
+        every { settingsService.getToken() } returns token
         coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream
         every {
-            metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, metadataOutputStream)
+            metadataService.onPackageBackedUp(packageInfo, BackupType.KV, metadataOutputStream)
         } just Runs
         every { metadataOutputStream.close() } just Runs
 
@@ -227,7 +233,7 @@ internal class BackupCoordinatorTest : BackupTest() {
         every { kv.getCurrentPackage() } returns pmPackageInfo
 
         coEvery { kv.finishBackup() } returns TRANSPORT_OK
-        every { settingsManager.canDoBackupNow() } returns false
+        every { settingsService.canDoBackupNow() } returns false
 
         assertEquals(TRANSPORT_OK, backup.finishBackup())
     }
@@ -240,10 +246,10 @@ internal class BackupCoordinatorTest : BackupTest() {
         every { full.hasState() } returns true
         every { full.getCurrentPackage() } returns packageInfo
         every { full.finishBackup() } returns result
-        every { settingsManager.getToken() } returns token
+        every { settingsService.getToken() } returns token
         coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream
         every {
-            metadataManager.onPackageBackedUp(packageInfo, BackupType.FULL, metadataOutputStream)
+            metadataService.onPackageBackedUp(packageInfo, BackupType.FULL, metadataOutputStream)
         } just Runs
         every { metadataOutputStream.close() } just Runs
 
@@ -254,20 +260,26 @@ internal class BackupCoordinatorTest : BackupTest() {
 
     @Test
     fun `metadata does not get updated when no APK was backed up`() = runBlocking {
-        every { settingsManager.getToken() } returns token
-        every { metadataManager.salt } returns salt
+        every { settingsService.getToken() } returns token
+        every { metadataService.salt } returns salt
         coEvery {
             full.performFullBackup(packageInfo, fileDescriptor, 0, token, salt)
         } returns TRANSPORT_OK
-        coEvery { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns null
+        coEvery {
+            apkBackupService.backupApkIfNecessary(
+                packageInfo,
+                UNKNOWN_ERROR,
+                any()
+            )
+        } returns null
 
         assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0))
     }
 
     @Test
     fun `app exceeding quota gets cancelled and reason written to metadata`() = runBlocking {
-        every { settingsManager.getToken() } returns token
-        every { metadataManager.salt } returns salt
+        every { settingsService.getToken() } returns token
+        every { metadataService.salt } returns salt
         coEvery {
             full.performFullBackup(packageInfo, fileDescriptor, 0, token, salt)
         } returns TRANSPORT_OK
@@ -278,7 +290,7 @@ internal class BackupCoordinatorTest : BackupTest() {
         } returns TRANSPORT_QUOTA_EXCEEDED
         every { full.getCurrentPackage() } returns packageInfo
         every {
-            metadataManager.onPackageBackupError(
+            metadataService.onPackageBackupError(
                 packageInfo,
                 QUOTA_EXCEEDED,
                 metadataOutputStream,
@@ -286,7 +298,7 @@ internal class BackupCoordinatorTest : BackupTest() {
             )
         } just Runs
         coEvery { full.cancelFullBackup(token, metadata.salt, false) } just Runs
-        every { settingsManager.getStorage() } returns storage
+        every { settingsService.getStorage() } returns storage
         every { metadataOutputStream.close() } just Runs
 
         assertEquals(
@@ -305,7 +317,7 @@ internal class BackupCoordinatorTest : BackupTest() {
         assertEquals(0L, backup.requestFullBackupTime())
 
         verify(exactly = 1) {
-            metadataManager.onPackageBackupError(
+            metadataService.onPackageBackupError(
                 packageInfo,
                 QUOTA_EXCEEDED,
                 metadataOutputStream,
@@ -317,8 +329,8 @@ internal class BackupCoordinatorTest : BackupTest() {
 
     @Test
     fun `app with no data gets cancelled and reason written to metadata`() = runBlocking {
-        every { settingsManager.getToken() } returns token
-        every { metadataManager.salt } returns salt
+        every { settingsService.getToken() } returns token
+        every { metadataService.salt } returns salt
         coEvery {
             full.performFullBackup(packageInfo, fileDescriptor, 0, token, salt)
         } returns TRANSPORT_OK
@@ -327,7 +339,7 @@ internal class BackupCoordinatorTest : BackupTest() {
         every { full.checkFullBackupSize(0) } returns TRANSPORT_PACKAGE_REJECTED
         every { full.getCurrentPackage() } returns packageInfo
         every {
-            metadataManager.onPackageBackupError(
+            metadataService.onPackageBackupError(
                 packageInfo,
                 NO_DATA,
                 metadataOutputStream,
@@ -335,7 +347,7 @@ internal class BackupCoordinatorTest : BackupTest() {
             )
         } just Runs
         coEvery { full.cancelFullBackup(token, metadata.salt, false) } just Runs
-        every { settingsManager.getStorage() } returns storage
+        every { settingsService.getStorage() } returns storage
         every { metadataOutputStream.close() } just Runs
 
         assertEquals(
@@ -351,7 +363,7 @@ internal class BackupCoordinatorTest : BackupTest() {
         assertEquals(0L, backup.requestFullBackupTime())
 
         verify(exactly = 1) {
-            metadataManager.onPackageBackupError(
+            metadataService.onPackageBackupError(
                 packageInfo,
                 NO_DATA,
                 metadataOutputStream,
@@ -376,10 +388,10 @@ internal class BackupCoordinatorTest : BackupTest() {
         )
         val packageMetadata: PackageMetadata = mockk()
 
-        every { settingsManager.canDoBackupNow() } returns true
-        every { metadataManager.requiresInit } returns false
-        every { settingsManager.getToken() } returns token
-        every { metadataManager.salt } returns salt
+        every { settingsService.canDoBackupNow() } returns true
+        every { metadataService.requiresInit } returns false
+        every { settingsService.getToken() } returns token
+        every { metadataService.salt } returns salt
         // do actual @pm@ backup
         coEvery {
             kv.performBackup(packageInfo, fileDescriptor, 0, token, salt)
@@ -395,7 +407,7 @@ internal class BackupCoordinatorTest : BackupTest() {
         every { full.hasState() } returns false
         every { kv.getCurrentPackage() } returns pmPackageInfo
         every {
-            metadataManager.onPackageBackedUp(pmPackageInfo, BackupType.KV, metadataOutputStream)
+            metadataService.onPackageBackedUp(pmPackageInfo, BackupType.KV, metadataOutputStream)
         } just Runs
         coEvery { kv.finishBackup() } returns TRANSPORT_OK
 
@@ -411,11 +423,11 @@ internal class BackupCoordinatorTest : BackupTest() {
         } just Runs
         // no backup needed
         coEvery {
-            apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any())
+            apkBackupService.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)
+            metadataService.getPackageMetadata(notAllowedPackages[0].packageName)
         } returns packageMetadata
         every { packageMetadata.state } returns NOT_ALLOWED // no change
 
@@ -429,12 +441,12 @@ internal class BackupCoordinatorTest : BackupTest() {
         } just Runs
         // was backed up, get new packageMetadata
         coEvery {
-            apkBackup.backupApkIfNecessary(notAllowedPackages[1], WAS_STOPPED, any())
+            apkBackupService.backupApkIfNecessary(notAllowedPackages[1], WAS_STOPPED, any())
         } returns packageMetadata
-        every { settingsManager.getToken() } returns token
+        every { settingsService.getToken() } returns token
         coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream
         every {
-            metadataManager.onApkBackedUp(
+            metadataService.onApkBackedUp(
                 notAllowedPackages[1],
                 packageMetadata,
                 metadataOutputStream
@@ -445,8 +457,8 @@ internal class BackupCoordinatorTest : BackupTest() {
         assertEquals(TRANSPORT_OK, backup.finishBackup())
 
         coVerify {
-            apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any())
-            apkBackup.backupApkIfNecessary(notAllowedPackages[1], WAS_STOPPED, any())
+            apkBackupService.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any())
+            apkBackupService.backupApkIfNecessary(notAllowedPackages[1], WAS_STOPPED, any())
             metadataOutputStream.close()
         }
     }
@@ -459,16 +471,22 @@ internal class BackupCoordinatorTest : BackupTest() {
         every {
             notificationManager.onOptOutAppBackup(packageInfo.packageName, 1, 1)
         } just Runs
-        coEvery { apkBackup.backupApkIfNecessary(packageInfo, NOT_ALLOWED, any()) } returns null
+        coEvery {
+            apkBackupService.backupApkIfNecessary(
+                packageInfo,
+                NOT_ALLOWED,
+                any()
+            )
+        } returns null
         every {
-            metadataManager.getPackageMetadata(packageInfo.packageName)
+            metadataService.getPackageMetadata(packageInfo.packageName)
         } returns oldPackageMetadata
         // state differs now, was stopped before
         every { oldPackageMetadata.state } returns WAS_STOPPED
-        every { settingsManager.getToken() } returns token
+        every { settingsService.getToken() } returns token
         coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream
         every {
-            metadataManager.onPackageBackupError(
+            metadataService.onPackageBackupError(
                 packageInfo,
                 NOT_ALLOWED,
                 metadataOutputStream
@@ -479,7 +497,7 @@ internal class BackupCoordinatorTest : BackupTest() {
         backup.backUpApksOfNotBackedUpPackages()
 
         verify {
-            metadataManager.onPackageBackupError(packageInfo, NOT_ALLOWED, metadataOutputStream)
+            metadataService.onPackageBackupError(packageInfo, NOT_ALLOWED, metadataOutputStream)
             metadataOutputStream.close()
         }
     }
@@ -490,14 +508,20 @@ internal class BackupCoordinatorTest : BackupTest() {
         every {
             notificationManager.onOptOutAppBackup(packageInfo.packageName, 1, 1)
         } just Runs
-        coEvery { apkBackup.backupApkIfNecessary(packageInfo, NOT_ALLOWED, any()) } returns null
-        every {
-            metadataManager.getPackageMetadata(packageInfo.packageName)
+        coEvery {
+            apkBackupService.backupApkIfNecessary(
+                packageInfo,
+                NOT_ALLOWED,
+                any()
+            )
         } returns null
-        every { settingsManager.getToken() } returns token
+        every {
+            metadataService.getPackageMetadata(packageInfo.packageName)
+        } returns null
+        every { settingsService.getToken() } returns token
         coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream
         every {
-            metadataManager.onPackageBackupError(
+            metadataService.onPackageBackupError(
                 packageInfo,
                 NOT_ALLOWED,
                 metadataOutputStream
@@ -508,23 +532,23 @@ internal class BackupCoordinatorTest : BackupTest() {
         backup.backUpApksOfNotBackedUpPackages()
 
         verify {
-            metadataManager.onPackageBackupError(packageInfo, NOT_ALLOWED, metadataOutputStream)
+            metadataService.onPackageBackupError(packageInfo, NOT_ALLOWED, metadataOutputStream)
             metadataOutputStream.close()
         }
     }
 
     private fun expectApkBackupAndMetadataWrite() {
         coEvery {
-            apkBackup.backupApkIfNecessary(
+            apkBackupService.backupApkIfNecessary(
                 any(),
                 UNKNOWN_ERROR,
                 any()
             )
         } returns packageMetadata
-        every { settingsManager.getToken() } returns token
+        every { settingsService.getToken() } returns token
         coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream
         every {
-            metadataManager.onApkBackedUp(
+            metadataService.onApkBackedUp(
                 any(),
                 packageMetadata,
                 metadataOutputStream
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupTest.kt
index b1f85b7c..6cdb6353 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupTest.kt
@@ -1,7 +1,9 @@
 package com.stevesoltys.seedvault.transport.backup
 
 import android.os.ParcelFileDescriptor
+import com.stevesoltys.seedvault.service.app.backup.InputFactory
 import com.stevesoltys.seedvault.transport.TransportTest
+import com.stevesoltys.seedvault.service.app.backup.full.DEFAULT_QUOTA_FULL_BACKUP
 import io.mockk.mockk
 import java.io.OutputStream
 
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupServiceTest.kt
similarity index 84%
rename from app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt
rename to app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupServiceTest.kt
index 2d03dd7e..6bb0a01f 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupServiceTest.kt
@@ -4,9 +4,11 @@ import android.app.backup.BackupTransport.TRANSPORT_ERROR
 import android.app.backup.BackupTransport.TRANSPORT_OK
 import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
 import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
-import com.stevesoltys.seedvault.header.VERSION
-import com.stevesoltys.seedvault.header.getADForFull
-import com.stevesoltys.seedvault.plugins.StoragePlugin
+import com.stevesoltys.seedvault.service.app.backup.full.DEFAULT_QUOTA_FULL_BACKUP
+import com.stevesoltys.seedvault.service.app.backup.full.FullBackupService
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.header.getADForFull
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
 import io.mockk.Runs
 import io.mockk.coEvery
 import io.mockk.every
@@ -22,10 +24,10 @@ import java.io.IOException
 import kotlin.random.Random
 
 @Suppress("BlockingMethodInNonBlockingContext")
-internal class FullBackupTest : BackupTest() {
+internal class FullBackupServiceTest : BackupTest() {
 
     private val plugin = mockk<StoragePlugin>()
-    private val backup = FullBackup(plugin, settingsManager, inputFactory, crypto)
+    private val backup = FullBackupService(plugin, settingsService, inputFactory, cryptoService)
 
     private val bytes = ByteArray(23).apply { Random.nextBytes(this) }
     private val inputStream = mockk<FileInputStream>()
@@ -38,7 +40,7 @@ internal class FullBackupTest : BackupTest() {
 
     @Test
     fun `checkFullBackupSize exceeds quota`() {
-        every { settingsManager.isQuotaUnlimited() } returns false
+        every { settingsService.isQuotaUnlimited() } returns false
 
         assertEquals(
             TRANSPORT_QUOTA_EXCEEDED,
@@ -48,7 +50,7 @@ internal class FullBackupTest : BackupTest() {
 
     @Test
     fun `checkFullBackupSize does not exceed quota when unlimited`() {
-        every { settingsManager.isQuotaUnlimited() } returns true
+        every { settingsService.isQuotaUnlimited() } returns true
 
         assertEquals(TRANSPORT_OK, backup.checkFullBackupSize(quota + 1))
     }
@@ -65,14 +67,14 @@ internal class FullBackupTest : BackupTest() {
 
     @Test
     fun `checkFullBackupSize accepts min data`() {
-        every { settingsManager.isQuotaUnlimited() } returns false
+        every { settingsService.isQuotaUnlimited() } returns false
 
         assertEquals(TRANSPORT_OK, backup.checkFullBackupSize(1))
     }
 
     @Test
     fun `checkFullBackupSize accepts max data`() {
-        every { settingsManager.isQuotaUnlimited() } returns false
+        every { settingsService.isQuotaUnlimited() } returns false
 
         assertEquals(TRANSPORT_OK, backup.checkFullBackupSize(quota))
     }
@@ -90,7 +92,7 @@ internal class FullBackupTest : BackupTest() {
 
     @Test
     fun `sendBackupData first call over quota`() = runBlocking {
-        every { settingsManager.isQuotaUnlimited() } returns false
+        every { settingsService.isQuotaUnlimited() } returns false
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         val numBytes = (quota + 1).toInt()
@@ -107,7 +109,7 @@ internal class FullBackupTest : BackupTest() {
 
     @Test
     fun `sendBackupData second call over quota`() = runBlocking {
-        every { settingsManager.isQuotaUnlimited() } returns false
+        every { settingsService.isQuotaUnlimited() } returns false
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         val numBytes1 = quota.toInt()
@@ -130,8 +132,8 @@ internal class FullBackupTest : BackupTest() {
     fun `sendBackupData throws exception when reading from InputStream`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
-        every { settingsManager.isQuotaUnlimited() } returns false
-        every { crypto.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream
+        every { settingsService.isQuotaUnlimited() } returns false
+        every { cryptoService.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream
         every { inputStream.read(any(), any(), bytes.size) } throws IOException()
         expectClearState()
 
@@ -147,8 +149,8 @@ internal class FullBackupTest : BackupTest() {
     fun `sendBackupData throws exception when getting outputStream`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
 
-        every { settingsManager.isQuotaUnlimited() } returns false
-        every { crypto.getNameForPackage(salt, packageInfo.packageName) } returns name
+        every { settingsService.isQuotaUnlimited() } returns false
+        every { cryptoService.getNameForPackage(salt, packageInfo.packageName) } returns name
         coEvery { plugin.getOutputStream(token, name) } throws IOException()
         expectClearState()
 
@@ -164,8 +166,8 @@ internal class FullBackupTest : BackupTest() {
     fun `sendBackupData throws exception when writing header`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
 
-        every { settingsManager.isQuotaUnlimited() } returns false
-        every { crypto.getNameForPackage(salt, packageInfo.packageName) } returns name
+        every { settingsService.isQuotaUnlimited() } returns false
+        every { cryptoService.getNameForPackage(salt, packageInfo.packageName) } returns name
         coEvery { plugin.getOutputStream(token, name) } returns outputStream
         every { inputFactory.getInputStream(data) } returns inputStream
         every { outputStream.write(ByteArray(1) { VERSION }) } throws IOException()
@@ -184,8 +186,13 @@ internal class FullBackupTest : BackupTest() {
         runBlocking {
             every { inputFactory.getInputStream(data) } returns inputStream
             expectInitializeOutputStream()
-            every { settingsManager.isQuotaUnlimited() } returns false
-            every { crypto.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream
+            every { settingsService.isQuotaUnlimited() } returns false
+            every {
+                cryptoService.newEncryptingStream(
+                    outputStream,
+                    ad
+                )
+            } returns encryptedOutputStream
             every { inputStream.read(any(), any(), bytes.size) } returns bytes.size
             every { encryptedOutputStream.write(any<ByteArray>()) } throws IOException()
             expectClearState()
@@ -200,7 +207,7 @@ internal class FullBackupTest : BackupTest() {
 
     @Test
     fun `sendBackupData runs ok`() = runBlocking {
-        every { settingsManager.isQuotaUnlimited() } returns false
+        every { settingsService.isQuotaUnlimited() } returns false
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         val numBytes1 = (quota / 2).toInt()
@@ -221,7 +228,7 @@ internal class FullBackupTest : BackupTest() {
 
     @Test
     fun `clearBackupData delegates to plugin`() = runBlocking {
-        every { crypto.getNameForPackage(salt, packageInfo.packageName) } returns name
+        every { cryptoService.getNameForPackage(salt, packageInfo.packageName) } returns name
         coEvery { plugin.removeData(token, name) } just Runs
 
         backup.clearBackupData(packageInfo, token, salt)
@@ -232,7 +239,7 @@ internal class FullBackupTest : BackupTest() {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         expectClearState()
-        every { crypto.getNameForPackage(salt, packageInfo.packageName) } returns name
+        every { cryptoService.getNameForPackage(salt, packageInfo.packageName) } returns name
         coEvery { plugin.removeData(token, name) } just Runs
 
         assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, data, 0, token, salt))
@@ -246,7 +253,7 @@ internal class FullBackupTest : BackupTest() {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         expectClearState()
-        every { crypto.getNameForPackage(salt, packageInfo.packageName) } returns name
+        every { cryptoService.getNameForPackage(salt, packageInfo.packageName) } returns name
         coEvery { plugin.removeData(token, name) } throws IOException()
 
         assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, data, 0, token, salt))
@@ -257,7 +264,7 @@ internal class FullBackupTest : BackupTest() {
 
     @Test
     fun `clearState throws exception when flushing OutputStream`() = runBlocking {
-        every { settingsManager.isQuotaUnlimited() } returns false
+        every { settingsService.isQuotaUnlimited() } returns false
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         val numBytes = 42
@@ -317,14 +324,14 @@ internal class FullBackupTest : BackupTest() {
     }
 
     private fun expectInitializeOutputStream() {
-        every { crypto.getNameForPackage(salt, packageInfo.packageName) } returns name
+        every { cryptoService.getNameForPackage(salt, packageInfo.packageName) } returns name
         coEvery { plugin.getOutputStream(token, name) } returns outputStream
         every { outputStream.write(ByteArray(1) { VERSION }) } just Runs
     }
 
     private fun expectSendData(numBytes: Int, readBytes: Int = numBytes) {
         every { inputStream.read(any(), any(), numBytes) } returns readBytes
-        every { crypto.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream
+        every { cryptoService.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream
         every { encryptedOutputStream.write(any<ByteArray>()) } just Runs
     }
 
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupServiceTest.kt
similarity index 90%
rename from app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt
rename to app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupServiceTest.kt
index e370f080..b0b81c7f 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupServiceTest.kt
@@ -9,10 +9,13 @@ import android.app.backup.BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUI
 import android.app.backup.BackupTransport.TRANSPORT_OK
 import android.content.pm.PackageInfo
 import com.stevesoltys.seedvault.getRandomString
-import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE
-import com.stevesoltys.seedvault.header.VERSION
-import com.stevesoltys.seedvault.header.getADForKV
-import com.stevesoltys.seedvault.plugins.StoragePlugin
+import com.stevesoltys.seedvault.service.header.MAX_KEY_LENGTH_SIZE
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.header.getADForKV
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.app.backup.kv.KVBackupService
+import com.stevesoltys.seedvault.service.app.backup.kv.KVDb
+import com.stevesoltys.seedvault.service.app.backup.kv.KvDbManager
 import io.mockk.CapturingSlot
 import io.mockk.Runs
 import io.mockk.coEvery
@@ -31,13 +34,14 @@ import java.io.IOException
 import kotlin.random.Random
 
 @Suppress("BlockingMethodInNonBlockingContext")
-internal class KVBackupTest : BackupTest() {
+internal class KVBackupServiceTest : BackupTest() {
 
     private val plugin = mockk<StoragePlugin>()
     private val dataInput = mockk<BackupDataInput>()
     private val dbManager = mockk<KvDbManager>()
 
-    private val backup = KVBackup(plugin, settingsManager, inputFactory, crypto, dbManager)
+    private val backup =
+        KVBackupService(plugin, settingsService, inputFactory, cryptoService, dbManager)
 
     private val db = mockk<KVDb>()
     private val packageName = packageInfo.packageName
@@ -106,7 +110,7 @@ internal class KVBackupTest : BackupTest() {
 
     @Test
     fun `package with no new data comes back ok right away`() = runBlocking {
-        every { crypto.getNameForPackage(salt, packageName) } returns name
+        every { cryptoService.getNameForPackage(salt, packageName) } returns name
         every { dbManager.getDb(packageName) } returns db
         every { data.close() } just Runs
 
@@ -214,7 +218,7 @@ internal class KVBackupTest : BackupTest() {
         coEvery { plugin.getOutputStream(token, name) } returns outputStream
         every { outputStream.write(ByteArray(1) { VERSION }) } just Runs
         val ad = getADForKV(VERSION, packageInfo.packageName)
-        every { crypto.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream
+        every { cryptoService.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream
         every { encryptedOutputStream.write(any<ByteArray>()) } throws IOException()
 
         assertEquals(TRANSPORT_ERROR, backup.finishBackup())
@@ -229,9 +233,9 @@ internal class KVBackupTest : BackupTest() {
     @Test
     fun `no upload when we back up @pm@ while we can't do backups`() = runBlocking {
         every { dbManager.existsDb(pmPackageInfo.packageName) } returns false
-        every { crypto.getNameForPackage(salt, pmPackageInfo.packageName) } returns name
+        every { cryptoService.getNameForPackage(salt, pmPackageInfo.packageName) } returns name
         every { dbManager.getDb(pmPackageInfo.packageName) } returns db
-        every { settingsManager.canDoBackupNow() } returns false
+        every { settingsService.canDoBackupNow() } returns false
         every { db.put(key, dataValue) } just Runs
         getDataInput(listOf(true, false))
 
@@ -258,7 +262,7 @@ internal class KVBackupTest : BackupTest() {
 
     private fun initPlugin(hasDataForPackage: Boolean = false, pi: PackageInfo = packageInfo) {
         every { dbManager.existsDb(pi.packageName) } returns hasDataForPackage
-        every { crypto.getNameForPackage(salt, pi.packageName) } returns name
+        every { cryptoService.getNameForPackage(salt, pi.packageName) } returns name
         every { dbManager.getDb(pi.packageName) } returns db
     }
 
@@ -285,7 +289,7 @@ internal class KVBackupTest : BackupTest() {
         coEvery { plugin.getOutputStream(token, name) } returns outputStream
         every { outputStream.write(ByteArray(1) { VERSION }) } just Runs
         val ad = getADForKV(VERSION, packageInfo.packageName)
-        every { crypto.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream
+        every { cryptoService.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream
         every { encryptedOutputStream.write(any<ByteArray>()) } just Runs // gzip header
         every { encryptedOutputStream.write(any(), any(), any()) } just Runs // stream copy
         every { dbManager.getDbInputStream(packageName) } returns inputStream
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/TestKvDbManager.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/TestKvDbManager.kt
index 7173f2ff..f70d3e6b 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/TestKvDbManager.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/TestKvDbManager.kt
@@ -3,6 +3,8 @@ package com.stevesoltys.seedvault.transport.backup
 import com.stevesoltys.seedvault.getRandomString
 import com.stevesoltys.seedvault.toByteArrayFromHex
 import com.stevesoltys.seedvault.toHexString
+import com.stevesoltys.seedvault.service.app.backup.kv.KVDb
+import com.stevesoltys.seedvault.service.app.backup.kv.KvDbManager
 import org.json.JSONObject
 import org.junit.jupiter.api.Assertions.assertArrayEquals
 import org.junit.jupiter.api.Assertions.assertEquals
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt
index 73044e0d..fa407a28 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt
@@ -6,13 +6,14 @@ import android.app.backup.BackupTransport.TRANSPORT_OK
 import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
 import com.stevesoltys.seedvault.coAssertThrows
 import com.stevesoltys.seedvault.getRandomByteArray
-import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
-import com.stevesoltys.seedvault.header.UnsupportedVersionException
-import com.stevesoltys.seedvault.header.VERSION
-import com.stevesoltys.seedvault.header.VersionHeader
-import com.stevesoltys.seedvault.header.getADForFull
-import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
-import com.stevesoltys.seedvault.plugins.StoragePlugin
+import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
+import com.stevesoltys.seedvault.service.header.MAX_SEGMENT_LENGTH
+import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.header.VersionHeader
+import com.stevesoltys.seedvault.service.header.getADForFull
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
 import io.mockk.CapturingSlot
 import io.mockk.Runs
 import io.mockk.coEvery
@@ -36,7 +37,8 @@ internal class FullRestoreTest : RestoreTest() {
 
     private val plugin = mockk<StoragePlugin>()
     private val legacyPlugin = mockk<LegacyStoragePlugin>()
-    private val restore = FullRestore(plugin, legacyPlugin, outputFactory, headerReader, crypto)
+    private val restore =
+        FullRestore(plugin, legacyPlugin, outputFactory, headerDecodeService, cryptoService)
 
     private val encrypted = getRandomByteArray()
     private val outputStream = ByteArrayOutputStream()
@@ -88,7 +90,7 @@ internal class FullRestoreTest : RestoreTest() {
         restore.initializeState(VERSION, token, name, packageInfo)
 
         coEvery { plugin.getInputStream(token, name) } returns inputStream
-        every { headerReader.readVersion(inputStream, VERSION) } throws IOException()
+        every { headerDecodeService.readVersion(inputStream, VERSION) } throws IOException()
         every { fileDescriptor.close() } just Runs
 
         assertEquals(
@@ -103,7 +105,7 @@ internal class FullRestoreTest : RestoreTest() {
 
         coEvery { plugin.getInputStream(token, name) } returns inputStream
         every {
-            headerReader.readVersion(inputStream, VERSION)
+            headerDecodeService.readVersion(inputStream, VERSION)
         } throws UnsupportedVersionException(unsupportedVersion)
         every { fileDescriptor.close() } just Runs
 
@@ -118,8 +120,8 @@ internal class FullRestoreTest : RestoreTest() {
         restore.initializeState(VERSION, token, name, packageInfo)
 
         coEvery { plugin.getInputStream(token, name) } returns inputStream
-        every { headerReader.readVersion(inputStream, VERSION) } returns VERSION
-        every { crypto.newDecryptingStream(inputStream, ad) } throws IOException()
+        every { headerDecodeService.readVersion(inputStream, VERSION) } returns VERSION
+        every { cryptoService.newDecryptingStream(inputStream, ad) } throws IOException()
         every { fileDescriptor.close() } just Runs
 
         assertEquals(
@@ -134,8 +136,13 @@ internal class FullRestoreTest : RestoreTest() {
             restore.initializeState(VERSION, token, name, packageInfo)
 
             coEvery { plugin.getInputStream(token, name) } returns inputStream
-            every { headerReader.readVersion(inputStream, VERSION) } returns VERSION
-            every { crypto.newDecryptingStream(inputStream, ad) } throws GeneralSecurityException()
+            every { headerDecodeService.readVersion(inputStream, VERSION) } returns VERSION
+            every {
+                cryptoService.newDecryptingStream(
+                    inputStream,
+                    ad
+                )
+            } throws GeneralSecurityException()
             every { fileDescriptor.close() } just Runs
 
             assertEquals(TRANSPORT_ERROR, restore.getNextFullRestoreDataChunk(fileDescriptor))
@@ -161,11 +168,11 @@ internal class FullRestoreTest : RestoreTest() {
         restore.initializeState(0.toByte(), token, name, packageInfo)
 
         coEvery { legacyPlugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
-        every { headerReader.readVersion(inputStream, 0.toByte()) } returns 0.toByte()
+        every { headerDecodeService.readVersion(inputStream, 0.toByte()) } returns 0.toByte()
         every {
-            crypto.decryptHeader(inputStream, 0.toByte(), packageInfo.packageName)
+            cryptoService.decryptHeader(inputStream, 0.toByte(), packageInfo.packageName)
         } returns VersionHeader(0.toByte(), packageInfo.packageName)
-        every { crypto.decryptSegment(inputStream) } returns encrypted
+        every { cryptoService.decryptSegment(inputStream) } returns encrypted
 
         every { outputFactory.getOutputStream(fileDescriptor) } returns outputStream
         every { fileDescriptor.close() } just Runs
@@ -183,7 +190,7 @@ internal class FullRestoreTest : RestoreTest() {
 
         coEvery { plugin.getInputStream(token, name) } returns inputStream
         every {
-            headerReader.readVersion(inputStream, Byte.MAX_VALUE)
+            headerDecodeService.readVersion(inputStream, Byte.MAX_VALUE)
         } throws GeneralSecurityException()
         every { inputStream.close() } just Runs
         every { fileDescriptor.close() } just Runs
@@ -200,8 +207,8 @@ internal class FullRestoreTest : RestoreTest() {
         restore.initializeState(VERSION, token, name, packageInfo)
 
         coEvery { plugin.getInputStream(token, name) } returns inputStream
-        every { headerReader.readVersion(inputStream, VERSION) } returns VERSION
-        every { crypto.newDecryptingStream(inputStream, ad) } returns decryptedInputStream
+        every { headerDecodeService.readVersion(inputStream, VERSION) } returns VERSION
+        every { cryptoService.newDecryptingStream(inputStream, ad) } returns decryptedInputStream
         every { outputFactory.getOutputStream(fileDescriptor) } returns outputStream
         every { fileDescriptor.close() } just Runs
         every { inputStream.close() } just Runs
@@ -233,8 +240,8 @@ internal class FullRestoreTest : RestoreTest() {
 
     private fun initInputStream() {
         coEvery { plugin.getInputStream(token, name) } returns inputStream
-        every { headerReader.readVersion(inputStream, VERSION) } returns VERSION
-        every { crypto.newDecryptingStream(inputStream, ad) } returns decryptedInputStream
+        every { headerDecodeService.readVersion(inputStream, VERSION) } returns VERSION
+        every { cryptoService.newDecryptingStream(inputStream, ad) } returns decryptedInputStream
     }
 
     private fun readAndEncryptInputStream(encryptedBytes: ByteArray) {
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt
index 678d6b63..bf654998 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt
@@ -4,16 +4,17 @@ import android.app.backup.BackupDataOutput
 import android.app.backup.BackupTransport.TRANSPORT_ERROR
 import android.app.backup.BackupTransport.TRANSPORT_OK
 import com.stevesoltys.seedvault.coAssertThrows
-import com.stevesoltys.seedvault.encodeBase64
+import com.stevesoltys.seedvault.util.encodeBase64
 import com.stevesoltys.seedvault.getRandomByteArray
-import com.stevesoltys.seedvault.header.UnsupportedVersionException
-import com.stevesoltys.seedvault.header.VERSION
-import com.stevesoltys.seedvault.header.VersionHeader
-import com.stevesoltys.seedvault.header.getADForKV
-import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
-import com.stevesoltys.seedvault.plugins.StoragePlugin
-import com.stevesoltys.seedvault.transport.backup.KVDb
-import com.stevesoltys.seedvault.transport.backup.KvDbManager
+import com.stevesoltys.seedvault.service.header.UnsupportedVersionException
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.header.VersionHeader
+import com.stevesoltys.seedvault.service.header.getADForKV
+import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
+import com.stevesoltys.seedvault.service.app.backup.kv.KVDb
+import com.stevesoltys.seedvault.service.app.backup.kv.KvDbManager
+import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
 import io.mockk.Runs
 import io.mockk.coEvery
 import io.mockk.every
@@ -40,8 +41,14 @@ internal class KVRestoreTest : RestoreTest() {
     private val legacyPlugin = mockk<LegacyStoragePlugin>()
     private val dbManager = mockk<KvDbManager>()
     private val output = mockk<BackupDataOutput>()
-    private val restore =
-        KVRestore(plugin, legacyPlugin, outputFactory, headerReader, crypto, dbManager)
+    private val restore = KVRestore(
+        plugin,
+        legacyPlugin,
+        outputFactory,
+        headerDecodeService,
+        cryptoService,
+        dbManager
+    )
 
     private val db = mockk<KVDb>()
     private val ad = getADForKV(VERSION, packageInfo.packageName)
@@ -75,7 +82,7 @@ internal class KVRestoreTest : RestoreTest() {
 
         coEvery { plugin.getInputStream(token, name) } returns inputStream
         every {
-            headerReader.readVersion(inputStream, VERSION)
+            headerDecodeService.readVersion(inputStream, VERSION)
         } throws UnsupportedVersionException(Byte.MAX_VALUE)
         every { dbManager.deleteDb(packageInfo.packageName, true) } returns true
         streamsGetClosed()
@@ -89,8 +96,13 @@ internal class KVRestoreTest : RestoreTest() {
         restore.initializeState(VERSION, token, name, packageInfo)
 
         coEvery { plugin.getInputStream(token, name) } returns inputStream
-        every { headerReader.readVersion(inputStream, VERSION) } returns VERSION
-        every { crypto.newDecryptingStream(inputStream, ad) } throws GeneralSecurityException()
+        every { headerDecodeService.readVersion(inputStream, VERSION) } returns VERSION
+        every {
+            cryptoService.newDecryptingStream(
+                inputStream,
+                ad
+            )
+        } throws GeneralSecurityException()
         every { dbManager.deleteDb(packageInfo.packageName, true) } returns true
         streamsGetClosed()
 
@@ -107,8 +119,8 @@ internal class KVRestoreTest : RestoreTest() {
         restore.initializeState(VERSION, token, name, packageInfo)
 
         coEvery { plugin.getInputStream(token, name) } returns inputStream
-        every { headerReader.readVersion(inputStream, VERSION) } returns VERSION
-        every { crypto.newDecryptingStream(inputStream, ad) } returns decryptInputStream
+        every { headerDecodeService.readVersion(inputStream, VERSION) } returns VERSION
+        every { cryptoService.newDecryptingStream(inputStream, ad) } returns decryptInputStream
         every {
             dbManager.getDbOutputStream(packageInfo.packageName)
         } returns ByteArrayOutputStream()
@@ -132,8 +144,8 @@ internal class KVRestoreTest : RestoreTest() {
         restore.initializeState(VERSION, token, name, packageInfo)
 
         coEvery { plugin.getInputStream(token, name) } returns inputStream
-        every { headerReader.readVersion(inputStream, VERSION) } returns VERSION
-        every { crypto.newDecryptingStream(inputStream, ad) } returns decryptInputStream
+        every { headerDecodeService.readVersion(inputStream, VERSION) } returns VERSION
+        every { cryptoService.newDecryptingStream(inputStream, ad) } returns decryptInputStream
         every {
             dbManager.getDbOutputStream(packageInfo.packageName)
         } returns ByteArrayOutputStream()
@@ -198,7 +210,7 @@ internal class KVRestoreTest : RestoreTest() {
             legacyPlugin.getInputStreamForRecord(token, packageInfo, key64)
         } returns inputStream
         every {
-            headerReader.readVersion(inputStream, 0x00)
+            headerDecodeService.readVersion(inputStream, 0x00)
         } throws UnsupportedVersionException(unsupportedVersion)
         streamsGetClosed()
 
@@ -214,7 +226,7 @@ internal class KVRestoreTest : RestoreTest() {
         coEvery {
             legacyPlugin.getInputStreamForRecord(token, packageInfo, key64)
         } returns inputStream
-        every { headerReader.readVersion(inputStream, 0x00) } throws IOException()
+        every { headerDecodeService.readVersion(inputStream, 0x00) } throws IOException()
         streamsGetClosed()
 
         assertEquals(TRANSPORT_ERROR, restore.getRestoreData(fileDescriptor))
@@ -230,9 +242,9 @@ internal class KVRestoreTest : RestoreTest() {
         coEvery {
             legacyPlugin.getInputStreamForRecord(token, packageInfo, key64)
         } returns inputStream
-        every { headerReader.readVersion(inputStream, 0x00) } returns 0x00
+        every { headerDecodeService.readVersion(inputStream, 0x00) } returns 0x00
         every {
-            crypto.decryptHeader(inputStream, 0x00, packageInfo.packageName, key)
+            cryptoService.decryptHeader(inputStream, 0x00, packageInfo.packageName, key)
         } throws IOException()
         streamsGetClosed()
 
@@ -249,11 +261,11 @@ internal class KVRestoreTest : RestoreTest() {
         coEvery {
             legacyPlugin.getInputStreamForRecord(token, packageInfo, key64)
         } returns inputStream
-        every { headerReader.readVersion(inputStream, 0x00) } returns 0x00
+        every { headerDecodeService.readVersion(inputStream, 0x00) } returns 0x00
         every {
-            crypto.decryptHeader(inputStream, 0x00, packageInfo.packageName, key)
+            cryptoService.decryptHeader(inputStream, 0x00, packageInfo.packageName, key)
         } returns VersionHeader(0x00, packageInfo.packageName, key)
-        every { crypto.decryptMultipleSegments(inputStream) } throws IOException()
+        every { cryptoService.decryptMultipleSegments(inputStream) } throws IOException()
         streamsGetClosed()
 
         assertEquals(TRANSPORT_ERROR, restore.getRestoreData(fileDescriptor))
@@ -269,11 +281,11 @@ internal class KVRestoreTest : RestoreTest() {
         coEvery {
             legacyPlugin.getInputStreamForRecord(token, packageInfo, key64)
         } returns inputStream
-        every { headerReader.readVersion(inputStream, 0) } returns 0
+        every { headerDecodeService.readVersion(inputStream, 0) } returns 0
         every {
-            crypto.decryptHeader(inputStream, 0x00, packageInfo.packageName, key)
+            cryptoService.decryptHeader(inputStream, 0x00, packageInfo.packageName, key)
         } returns VersionHeader(0x00, packageInfo.packageName, key)
-        every { crypto.decryptMultipleSegments(inputStream) } returns data
+        every { cryptoService.decryptMultipleSegments(inputStream) } returns data
         every { output.writeEntityHeader(key, data.size) } throws IOException()
         streamsGetClosed()
 
@@ -290,11 +302,11 @@ internal class KVRestoreTest : RestoreTest() {
         coEvery {
             legacyPlugin.getInputStreamForRecord(token, packageInfo, key64)
         } returns inputStream
-        every { headerReader.readVersion(inputStream, 0) } returns 0
+        every { headerDecodeService.readVersion(inputStream, 0) } returns 0
         every {
-            crypto.decryptHeader(inputStream, 0, packageInfo.packageName, key)
+            cryptoService.decryptHeader(inputStream, 0, packageInfo.packageName, key)
         } returns VersionHeader(0, packageInfo.packageName, key)
-        every { crypto.decryptMultipleSegments(inputStream) } returns data
+        every { cryptoService.decryptMultipleSegments(inputStream) } returns data
         every { output.writeEntityHeader(key, data.size) } returns 42
         every { output.writeEntityData(data, data.size) } throws IOException()
         streamsGetClosed()
@@ -312,11 +324,11 @@ internal class KVRestoreTest : RestoreTest() {
         coEvery {
             legacyPlugin.getInputStreamForRecord(token, packageInfo, key64)
         } returns inputStream
-        every { headerReader.readVersion(inputStream, 0) } returns 0
+        every { headerDecodeService.readVersion(inputStream, 0) } returns 0
         every {
-            crypto.decryptHeader(inputStream, 0, packageInfo.packageName, key)
+            cryptoService.decryptHeader(inputStream, 0, packageInfo.packageName, key)
         } returns VersionHeader(0, packageInfo.packageName, key)
-        every { crypto.decryptMultipleSegments(inputStream) } returns data
+        every { cryptoService.decryptMultipleSegments(inputStream) } returns data
         every { output.writeEntityHeader(key, data.size) } returns 42
         every { output.writeEntityData(data, data.size) } returns data.size
         streamsGetClosed()
@@ -334,11 +346,11 @@ internal class KVRestoreTest : RestoreTest() {
         coEvery {
             legacyPlugin.getInputStreamForRecord(token, packageInfo, key64)
         } returns inputStream
-        every { headerReader.readVersion(inputStream, 0) } returns 0
+        every { headerDecodeService.readVersion(inputStream, 0) } returns 0
         every {
-            crypto.decryptHeader(inputStream, 0, packageInfo.packageName, key)
+            cryptoService.decryptHeader(inputStream, 0, packageInfo.packageName, key)
         } returns VersionHeader(VERSION, packageInfo.packageName, key)
-        every { crypto.decryptMultipleSegments(inputStream) } returns data
+        every { cryptoService.decryptMultipleSegments(inputStream) } returns data
         every { output.writeEntityHeader(key, data.size) } returns 42
         every { output.writeEntityData(data, data.size) } returns data.size
         streamsGetClosed()
@@ -359,22 +371,22 @@ internal class KVRestoreTest : RestoreTest() {
         coEvery {
             legacyPlugin.getInputStreamForRecord(token, packageInfo, key64)
         } returns inputStream
-        every { headerReader.readVersion(inputStream, 0) } returns 0
+        every { headerDecodeService.readVersion(inputStream, 0) } returns 0
         every {
-            crypto.decryptHeader(inputStream, 0, packageInfo.packageName, key)
+            cryptoService.decryptHeader(inputStream, 0, packageInfo.packageName, key)
         } returns VersionHeader(0, packageInfo.packageName, key)
-        every { crypto.decryptMultipleSegments(inputStream) } returns data
+        every { cryptoService.decryptMultipleSegments(inputStream) } returns data
         every { output.writeEntityHeader(key, data.size) } returns 42
         every { output.writeEntityData(data, data.size) } returns data.size
         // second key/value
         coEvery {
             legacyPlugin.getInputStreamForRecord(token, packageInfo, key264)
         } returns inputStream2
-        every { headerReader.readVersion(inputStream2, 0) } returns 0
+        every { headerDecodeService.readVersion(inputStream2, 0) } returns 0
         every {
-            crypto.decryptHeader(inputStream2, 0, packageInfo.packageName, key2)
+            cryptoService.decryptHeader(inputStream2, 0, packageInfo.packageName, key2)
         } returns VersionHeader(0, packageInfo.packageName, key2)
-        every { crypto.decryptMultipleSegments(inputStream2) } returns data2
+        every { cryptoService.decryptMultipleSegments(inputStream2) } returns data2
         every { output.writeEntityHeader(key2, data2.size) } returns 42
         every { output.writeEntityData(data2, data2.size) } returns data2.size
         every { inputStream2.close() } just Runs
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 88ba4c15..89157119 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
@@ -10,13 +10,17 @@ import android.content.pm.PackageInfo
 import android.os.ParcelFileDescriptor
 import com.stevesoltys.seedvault.coAssertThrows
 import com.stevesoltys.seedvault.getRandomString
-import com.stevesoltys.seedvault.header.VERSION
-import com.stevesoltys.seedvault.metadata.BackupType
-import com.stevesoltys.seedvault.metadata.MetadataReader
-import com.stevesoltys.seedvault.metadata.PackageMetadata
-import com.stevesoltys.seedvault.plugins.EncryptedMetadata
-import com.stevesoltys.seedvault.plugins.StoragePlugin
-import com.stevesoltys.seedvault.settings.Storage
+import com.stevesoltys.seedvault.service.app.restore.coordinator.D2D_DEVICE_NAME
+import com.stevesoltys.seedvault.service.app.restore.coordinator.RestoreCoordinator
+import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
+import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
+import com.stevesoltys.seedvault.service.header.VERSION
+import com.stevesoltys.seedvault.service.metadata.BackupType
+import com.stevesoltys.seedvault.service.metadata.MetadataReader
+import com.stevesoltys.seedvault.service.metadata.PackageMetadata
+import com.stevesoltys.seedvault.service.settings.Storage
+import com.stevesoltys.seedvault.service.storage.EncryptedBackupMetadata
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
 import com.stevesoltys.seedvault.transport.TransportTest
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import io.mockk.Runs
@@ -46,9 +50,9 @@ internal class RestoreCoordinatorTest : TransportTest() {
 
     private val restore = RestoreCoordinator(
         context,
-        crypto,
-        settingsManager,
-        metadataManager,
+        cryptoService,
+        settingsService,
+        metadataService,
         notificationManager,
         plugin,
         kv,
@@ -75,11 +79,11 @@ internal class RestoreCoordinatorTest : TransportTest() {
 
     @Test
     fun `getAvailableRestoreSets() builds set from plugin response`() = runBlocking {
-        val encryptedMetadata = EncryptedMetadata(token) { inputStream }
+        val encryptedBackupMetadata = EncryptedBackupMetadata(token) { inputStream }
 
         coEvery { plugin.getAvailableBackups() } returns sequenceOf(
-            encryptedMetadata,
-            EncryptedMetadata(token + 1) { inputStream }
+            encryptedBackupMetadata,
+            EncryptedBackupMetadata(token + 1) { inputStream }
         )
         every { metadataReader.readMetadata(inputStream, token) } returns metadata
         every { metadataReader.readMetadata(inputStream, token + 1) } returns metadata
@@ -103,7 +107,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
 
     @Test
     fun `getCurrentRestoreSet() delegates to plugin`() {
-        every { settingsManager.getToken() } returns token
+        every { settingsService.getToken() } returns token
         assertEquals(token, restore.getCurrentRestoreSet())
     }
 
@@ -116,8 +120,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
     @Test
     fun `startRestore() fetches metadata if missing`() = runBlocking {
         coEvery { plugin.getAvailableBackups() } returns sequenceOf(
-            EncryptedMetadata(token) { inputStream },
-            EncryptedMetadata(token + 1) { inputStream }
+            EncryptedBackupMetadata(token) { inputStream },
+            EncryptedBackupMetadata(token + 1) { inputStream }
         )
         every { metadataReader.readMetadata(inputStream, token) } returns metadata
         every { metadataReader.readMetadata(inputStream, token + 1) } returns metadata
@@ -129,7 +133,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
     @Test
     fun `startRestore() errors if metadata is not matching token`() = runBlocking {
         coEvery { plugin.getAvailableBackups() } returns sequenceOf(
-            EncryptedMetadata(token + 42) { inputStream }
+            EncryptedBackupMetadata(token + 42) { inputStream }
         )
         every { metadataReader.readMetadata(inputStream, token + 42) } returns metadata
         every { inputStream.close() } just Runs
@@ -164,9 +168,9 @@ internal class RestoreCoordinatorTest : TransportTest() {
     @Test
     fun `startRestore() optimized auto-restore with removed storage shows notification`() =
         runBlocking {
-            every { settingsManager.getStorage() } returns storage
+            every { settingsService.getStorage() } returns storage
             every { storage.isUnavailableUsb(context) } returns true
-            every { metadataManager.getPackageMetadata(packageName) } returns PackageMetadata(42L)
+            every { metadataService.getPackageMetadata(packageName) } returns PackageMetadata(42L)
             every { storage.name } returns storageName
             every {
                 notificationManager.onRemovableStorageNotAvailableForRestore(
@@ -188,7 +192,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
     @Test
     fun `startRestore() optimized auto-restore with available storage shows no notification`() =
         runBlocking {
-            every { settingsManager.getStorage() } returns storage
+            every { settingsService.getStorage() } returns storage
             every { storage.isUnavailableUsb(context) } returns false
 
             restore.beforeStartRestore(metadata)
@@ -204,9 +208,9 @@ internal class RestoreCoordinatorTest : TransportTest() {
 
     @Test
     fun `startRestore() with removed storage shows no notification`() = runBlocking {
-        every { settingsManager.getStorage() } returns storage
+        every { settingsService.getStorage() } returns storage
         every { storage.isUnavailableUsb(context) } returns true
-        every { metadataManager.getPackageMetadata(packageName) } returns null
+        every { metadataService.getPackageMetadata(packageName) } returns null
 
         assertEquals(TRANSPORT_ERROR, restore.startRestore(token, pmPackageInfoArray))
 
@@ -230,7 +234,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
         restore.beforeStartRestore(metadata)
         restore.startRestore(token, packageInfoArray)
 
-        every { crypto.getNameForPackage(metadata.salt, packageName) } returns name
+        every { cryptoService.getNameForPackage(metadata.salt, packageName) } returns name
         coEvery { plugin.hasData(token, name) } returns true
         every { kv.initializeState(VERSION, token, name, packageInfo) } just Runs
 
@@ -270,9 +274,14 @@ internal class RestoreCoordinatorTest : TransportTest() {
         restore.beforeStartRestore(metadata)
         restore.startRestore(token, packageInfoArray2)
 
-        every { crypto.getNameForPackage(metadata.salt, packageName) } returns name
+        every { cryptoService.getNameForPackage(metadata.salt, packageName) } returns name
         coEvery { plugin.hasData(token, name) } returns false
-        every { crypto.getNameForPackage(metadata.salt, packageInfo2.packageName) } returns name2
+        every {
+            cryptoService.getNameForPackage(
+                metadata.salt,
+                packageInfo2.packageName
+            )
+        } returns name2
         coEvery { plugin.hasData(token, name2) } returns false
 
         assertEquals(NO_MORE_PACKAGES, restore.nextRestorePackage())
@@ -285,7 +294,12 @@ internal class RestoreCoordinatorTest : TransportTest() {
         restore.beforeStartRestore(metadata)
         restore.startRestore(token, packageInfoArray2)
 
-        every { crypto.getNameForPackage(metadata.salt, packageInfo2.packageName) } returns name2
+        every {
+            cryptoService.getNameForPackage(
+                metadata.salt,
+                packageInfo2.packageName
+            )
+        } returns name2
         coEvery { plugin.hasData(token, name2) } returns true
         every { full.initializeState(VERSION, token, name2, packageInfo2) } just Runs
 
@@ -300,14 +314,19 @@ internal class RestoreCoordinatorTest : TransportTest() {
         restore.beforeStartRestore(metadata)
         restore.startRestore(token, packageInfoArray2)
 
-        every { crypto.getNameForPackage(metadata.salt, packageName) } returns name
+        every { cryptoService.getNameForPackage(metadata.salt, packageName) } returns name
         coEvery { plugin.hasData(token, name) } returns true
         every { kv.initializeState(VERSION, token, name, packageInfo) } just Runs
 
         val expected = RestoreDescription(packageInfo.packageName, TYPE_KEY_VALUE)
         assertEquals(expected, restore.nextRestorePackage())
 
-        every { crypto.getNameForPackage(metadata.salt, packageInfo2.packageName) } returns name2
+        every {
+            cryptoService.getNameForPackage(
+                metadata.salt,
+                packageInfo2.packageName
+            )
+        } returns name2
         coEvery { plugin.hasData(token, name2) } returns true
         every { full.initializeState(VERSION, token, name2, packageInfo2) } just Runs
 
@@ -356,9 +375,14 @@ internal class RestoreCoordinatorTest : TransportTest() {
         restore.beforeStartRestore(metadata)
         restore.startRestore(token, packageInfoArray2)
 
-        every { crypto.getNameForPackage(metadata.salt, packageName) } returns name
+        every { cryptoService.getNameForPackage(metadata.salt, packageName) } returns name
         coEvery { plugin.hasData(token, name) } returns false
-        every { crypto.getNameForPackage(metadata.salt, packageInfo2.packageName) } returns name2
+        every {
+            cryptoService.getNameForPackage(
+                metadata.salt,
+                packageInfo2.packageName
+            )
+        } returns name2
         coEvery { plugin.hasData(token, name2) } throws IOException()
 
         assertEquals(NO_MORE_PACKAGES, restore.nextRestorePackage())
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreTest.kt
index e720bc79..157c8ffb 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreTest.kt
@@ -2,8 +2,9 @@ package com.stevesoltys.seedvault.transport.restore
 
 import android.os.ParcelFileDescriptor
 import com.stevesoltys.seedvault.getRandomByteArray
-import com.stevesoltys.seedvault.header.HeaderReader
-import com.stevesoltys.seedvault.header.VERSION
+import com.stevesoltys.seedvault.service.app.restore.OutputFactory
+import com.stevesoltys.seedvault.service.header.HeaderDecodeService
+import com.stevesoltys.seedvault.service.header.VERSION
 import com.stevesoltys.seedvault.transport.TransportTest
 import io.mockk.mockk
 import java.io.InputStream
@@ -11,7 +12,7 @@ import java.io.InputStream
 internal abstract class RestoreTest : TransportTest() {
 
     protected val outputFactory = mockk<OutputFactory>()
-    protected val headerReader = mockk<HeaderReader>()
+    protected val headerDecodeService = mockk<HeaderDecodeService>()
     protected val fileDescriptor = mockk<ParcelFileDescriptor>()
 
     protected val data = getRandomByteArray()
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt
index 07df8f34..86899bc9 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt
@@ -6,18 +6,22 @@ import android.app.backup.BackupTransport.TRANSPORT_OK
 import android.app.backup.RestoreDescription
 import android.app.backup.RestoreDescription.TYPE_FULL_STREAM
 import android.os.ParcelFileDescriptor
-import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
-import com.stevesoltys.seedvault.crypto.CryptoImpl
-import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES
+import com.stevesoltys.seedvault.service.crypto.CipherFactoryImpl
+import com.stevesoltys.seedvault.service.crypto.CryptoServiceImpl
+import com.stevesoltys.seedvault.service.crypto.KEY_SIZE_BYTES
 import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
-import com.stevesoltys.seedvault.encodeBase64
-import com.stevesoltys.seedvault.header.HeaderReaderImpl
-import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
-import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
-import com.stevesoltys.seedvault.plugins.StoragePlugin
+import com.stevesoltys.seedvault.util.encodeBase64
+import com.stevesoltys.seedvault.service.header.HeaderDecodeServiceImpl
+import com.stevesoltys.seedvault.service.metadata.MetadataReaderImpl
+import com.stevesoltys.seedvault.service.storage.saf.legacy.LegacyStoragePlugin
+import com.stevesoltys.seedvault.service.storage.StoragePlugin
 import com.stevesoltys.seedvault.toByteArrayFromHex
 import com.stevesoltys.seedvault.transport.TransportTest
-import com.stevesoltys.seedvault.transport.backup.KvDbManager
+import com.stevesoltys.seedvault.service.app.backup.kv.KvDbManager
+import com.stevesoltys.seedvault.service.app.restore.OutputFactory
+import com.stevesoltys.seedvault.service.app.restore.coordinator.RestoreCoordinator
+import com.stevesoltys.seedvault.service.app.restore.full.FullRestore
+import com.stevesoltys.seedvault.service.app.restore.kv.KVRestore
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import io.mockk.coEvery
 import io.mockk.every
@@ -44,10 +48,10 @@ internal class RestoreV0IntegrationTest : TransportTest() {
     )
     private val keyManager = KeyManagerTestImpl(secretKey)
     private val cipherFactory = CipherFactoryImpl(keyManager)
-    private val headerReader = HeaderReaderImpl()
-    private val cryptoImpl = CryptoImpl(keyManager, cipherFactory, headerReader)
+    private val headerReader = HeaderDecodeServiceImpl()
+    private val cryptoServiceImpl = CryptoServiceImpl(keyManager, cipherFactory, headerReader)
     private val dbManager = mockk<KvDbManager>()
-    private val metadataReader = MetadataReaderImpl(cryptoImpl)
+    private val metadataReader = MetadataReaderImpl(cryptoServiceImpl)
     private val notificationManager = mockk<BackupNotificationManager>()
 
     @Suppress("Deprecation")
@@ -58,16 +62,16 @@ internal class RestoreV0IntegrationTest : TransportTest() {
         legacyPlugin,
         outputFactory,
         headerReader,
-        cryptoImpl,
+        cryptoServiceImpl,
         dbManager
     )
     private val fullRestore =
-        FullRestore(backupPlugin, legacyPlugin, outputFactory, headerReader, cryptoImpl)
+        FullRestore(backupPlugin, legacyPlugin, outputFactory, headerReader, cryptoServiceImpl)
     private val restore = RestoreCoordinator(
         context,
-        crypto,
-        settingsManager,
-        metadataManager,
+        cryptoService,
+        settingsService,
+        metadataService,
         notificationManager,
         backupPlugin,
         kvRestore,