diff --git a/mailbox-android/src/main/AndroidManifest.xml b/mailbox-android/src/main/AndroidManifest.xml
index 30bcdd515ab75017ad760198f5ecfed04d71b2bf..cb70c9be8fe1298cc5fe8c92c7c02837517978b1 100644
--- a/mailbox-android/src/main/AndroidManifest.xml
+++ b/mailbox-android/src/main/AndroidManifest.xml
@@ -22,8 +22,6 @@
         android:supportsRtl="true"
         android:theme="@style/Theme.BriarMailbox">
 
-        <service android:name=".android.MailboxService" />
-
         <activity
             android:name=".android.ui.MainActivity"
             android:exported="true"
@@ -35,13 +33,28 @@
             </intent-filter>
         </activity>
         <activity
-            android:name=".android.ui.WipeCompleteActivity"
+            android:name=".android.ui.settings.SettingsActivity"
+            android:exported="false"
+            android:label="@string/prefs_title" />
+        <activity
+            android:name=".android.ui.wipe.WipeCompleteActivity"
+            android:excludeFromRecents="true"
+            android:launchMode="singleInstance"
+            android:process=":briar_mailbox_wipe_complete"
+            android:theme="@style/Theme.BriarMailbox.NoActionBar" />
+        <activity
+            android:name=".android.ui.startup.StartupFailureActivity"
             android:excludeFromRecents="true"
+            android:exported="false"
+            android:finishOnTaskLaunch="true"
+            android:label="@string/startup_failed_activity_title"
             android:launchMode="singleInstance"
-            android:process=":briar_mailbox_wipe_complete" />
+            android:process=":briar_mailbox_startup_failure"
+            android:theme="@style/Theme.BriarMailbox.NoActionBar" />
 
-        <receiver android:name=".core.system.AlarmReceiver" />
+        <service android:name=".android.MailboxService" />
 
+        <receiver android:name=".core.system.AlarmReceiver" />
         <receiver
             android:name=".android.StartReceiver"
             android:exported="false">
@@ -50,15 +63,6 @@
                 <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
             </intent-filter>
         </receiver>
-
-        <activity
-            android:name=".android.ui.StartupFailureActivity"
-            android:excludeFromRecents="true"
-            android:exported="false"
-            android:finishOnTaskLaunch="true"
-            android:label="@string/startup_failed_activity_title"
-            android:launchMode="singleInstance"
-            android:process=":briar_mailbox_startup_failure" />
     </application>
 
-</manifest>
+</manifest>
\ No newline at end of file
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxPreferences.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxPreferences.kt
index ce7a571f30e5f8fb1a02bc45e4aef6539373fd3a..859e43c988775bb00d8cee6bd8cd3369af2863e5 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxPreferences.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxPreferences.kt
@@ -24,7 +24,7 @@ import android.content.SharedPreferences
 import androidx.core.content.edit
 import androidx.preference.PreferenceManager.getDefaultSharedPreferences
 import dagger.hilt.android.qualifiers.ApplicationContext
-import org.briarproject.mailbox.android.ui.WipeCompleteFragment
+import org.briarproject.mailbox.android.ui.wipe.WipeCompleteFragment
 import javax.inject.Inject
 
 class MailboxPreferences @Inject constructor(@ApplicationContext val context: Context) {
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
index 5d94053ba090f1db43cd022dedb0236384c94ab5..2ab01db223fa2b1d26a1aa00e3c2d2645c6986ef 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
@@ -35,10 +35,10 @@ import org.briarproject.android.dontkillmelib.wakelock.AndroidWakeLockManager
 import org.briarproject.mailbox.R
 import org.briarproject.mailbox.android.MailboxNotificationManager.Companion.NOTIFICATION_MAIN_ID
 import org.briarproject.mailbox.android.StatusManager.Starting
-import org.briarproject.mailbox.android.ui.StartupFailureActivity
-import org.briarproject.mailbox.android.ui.StartupFailureActivity.Companion.EXTRA_START_RESULT
-import org.briarproject.mailbox.android.ui.StartupFailureActivity.StartupFailure
-import org.briarproject.mailbox.android.ui.WipeCompleteActivity
+import org.briarproject.mailbox.android.ui.startup.StartupFailureActivity
+import org.briarproject.mailbox.android.ui.startup.StartupFailureActivity.Companion.EXTRA_START_RESULT
+import org.briarproject.mailbox.android.ui.startup.StartupFailureActivity.StartupFailure
+import org.briarproject.mailbox.android.ui.wipe.WipeCompleteActivity
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.CLOCK_ERROR
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.LIFECYCLE_REUSE
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/BackFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/BackFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..01ec85c71a5428bbe93fae8742ded3ba31a34e35
--- /dev/null
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/BackFragment.kt
@@ -0,0 +1,34 @@
+package org.briarproject.mailbox.android.ui
+
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import androidx.annotation.CallSuper
+import androidx.core.view.MenuProvider
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle.State.RESUMED
+
+/**
+ * A fragment that allows back navigation via the action bar.
+ */
+abstract class BackFragment : Fragment(), MenuProvider {
+    @CallSuper
+    override fun onViewCreated(v: View, savedInstanceState: Bundle?) {
+        // only needed for up/back navigation in action bar
+        requireActivity().addMenuProvider(this, viewLifecycleOwner, RESUMED)
+    }
+
+    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+    }
+
+    @CallSuper
+    override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+        if (menuItem.itemId == android.R.id.home) {
+            requireActivity().onBackPressedDispatcher.onBackPressed()
+            return true
+        }
+        return false
+    }
+}
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/MailboxViewModel.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/MailboxViewModel.kt
index 841a0794760f8a21f59c69c107d20ddcff71ef41..40203d840bc90e2367161e85fe870a7f6cac2d30 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/MailboxViewModel.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/MailboxViewModel.kt
@@ -37,6 +37,7 @@ import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState
 import org.briarproject.mailbox.core.settings.MetadataManager
 import org.briarproject.mailbox.core.system.AndroidExecutor
 import org.briarproject.mailbox.core.system.DozeWatchdog
+import org.briarproject.mailbox.core.tor.TorPlugin
 import org.briarproject.mailbox.core.util.LogUtils.info
 import org.slf4j.LoggerFactory.getLogger
 import javax.inject.Inject
@@ -51,6 +52,7 @@ class MailboxViewModel @Inject constructor(
     metadataManager: MetadataManager,
     private val mailboxPreferences: MailboxPreferences,
     private val androidExecutor: AndroidExecutor,
+    private val torPlugin: TorPlugin,
     handle: SavedStateHandle,
 ) : AndroidViewModel(app) {
 
@@ -116,6 +118,10 @@ class MailboxViewModel @Inject constructor(
         MailboxService.stopService(getApplication())
     }
 
+    fun onSettingsChanged() {
+        torPlugin.onSettingsChanged()
+    }
+
     /**
      * Called from the status fragment's unlink button.
      */
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/MainActivity.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/MainActivity.kt
index fb99d10f6622c005c622ff8e6757df693e19aec4..d2a600fa33bc5ab390307be8feea96ba36555056 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/MainActivity.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/MainActivity.kt
@@ -23,12 +23,18 @@ import android.Manifest.permission.POST_NOTIFICATIONS
 import android.content.Intent
 import android.os.Build.VERSION.SDK_INT
 import android.os.Bundle
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
 import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
 import androidx.activity.viewModels
+import androidx.annotation.UiThread
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
 import androidx.core.content.PermissionChecker.checkSelfPermission
+import androidx.core.view.MenuProvider
+import androidx.lifecycle.Lifecycle.State.RESUMED
 import androidx.navigation.NavController
 import androidx.navigation.fragment.NavHostFragment
 import dagger.hilt.android.AndroidEntryPoint
@@ -57,12 +63,14 @@ import org.briarproject.mailbox.android.StatusManager.Stopped
 import org.briarproject.mailbox.android.StatusManager.Stopping
 import org.briarproject.mailbox.android.StatusManager.Undecided
 import org.briarproject.mailbox.android.StatusManager.Wiping
+import org.briarproject.mailbox.android.ui.settings.SettingsActivity
+import org.briarproject.mailbox.android.ui.wipe.WipeCompleteActivity
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.NOT_STARTED
 import org.briarproject.mailbox.core.util.LogUtils.info
 import org.slf4j.LoggerFactory.getLogger
 
 @AndroidEntryPoint
-class MainActivity : AppCompatActivity() {
+class MainActivity : AppCompatActivity(), MenuProvider {
 
     companion object {
         private val LOG = getLogger(MainActivity::class.java)
@@ -87,6 +95,8 @@ class MainActivity : AppCompatActivity() {
         LOG.info("onCreate()")
         setContentView(R.layout.activity_main)
 
+        addMenuProvider(this, this, RESUMED)
+
         LOG.info { "do we have a saved instance state? " + (savedInstanceState != null) }
         hadBeenStartedOnSave =
             savedInstanceState?.getBoolean(BUNDLE_LIFECYCLE_HAS_STARTED) ?: false
@@ -101,8 +111,7 @@ class MainActivity : AppCompatActivity() {
                     supportActionBar?.setDisplayHomeAsUpEnabled(true)
                     getString(R.string.link_text_title)
                 }
-                R.id.statusFragment -> getString(R.string.app_name)
-                else -> ""
+                else -> getString(R.string.app_name)
             }
         }
 
@@ -111,6 +120,19 @@ class MainActivity : AppCompatActivity() {
         }
     }
 
+    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+        menuInflater.inflate(R.menu.main_actions, menu)
+    }
+
+    override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+        if (menuItem.itemId == R.id.action_settings) {
+            startActivity(Intent(this, SettingsActivity::class.java))
+            return true
+        }
+        return false
+    }
+
+    @UiThread
     private fun onAppStateChanged(state: MailboxAppState) {
         // Catch the situation where we come back to the activity after a remote wipe has happened
         // while the app was in the background and gets restored from the recent app list after
@@ -122,21 +144,20 @@ class MainActivity : AppCompatActivity() {
             startActivity(Intent(this, WipeCompleteActivity::class.java))
             return
         }
+        val currentDestId = nav.currentDestination?.id
         when (state) {
             Undecided -> supportActionBar?.hide() // hide action bar until we need it
-            NeedOnboarding -> {
+            NeedOnboarding -> if (currentDestId == R.id.initFragment) {
                 supportActionBar?.hide()
-                if (nav.currentDestination?.id == R.id.initFragment)
-                    nav.navigate(actionGlobalOnboardingContainer())
+                nav.navigate(actionGlobalOnboardingContainer())
             }
-            NeedsDozeExemption -> {
+            NeedsDozeExemption -> if (currentDestId != R.id.doNotKillMeFragment) {
                 supportActionBar?.hide()
-                if (nav.currentDestination?.id != R.id.doNotKillMeFragment)
-                    nav.navigate(actionGlobalDoNotKillMeFragment())
+                nav.navigate(actionGlobalDoNotKillMeFragment())
             }
             NotStarted -> {
                 askForNotificationPermission()
-                supportActionBar?.hide()
+                supportActionBar?.show()
                 nav.navigate(actionGlobalStartupFragment())
             }
             // It is important to navigate here from various fragments. The normal case is
@@ -144,47 +165,40 @@ class MainActivity : AppCompatActivity() {
             // However, when the service got killed and the app has been restored with a different
             // UI state such as the qr code screen or the status screen, then we also want to
             // navigate to the startup fragment.
-            is Starting -> {
-                supportActionBar?.hide()
-                if (nav.currentDestination?.id != R.id.startupFragment)
-                    nav.navigate(actionGlobalStartupFragment())
+            is Starting -> if (currentDestId != R.id.startupFragment) {
+                supportActionBar?.show()
+                nav.navigate(actionGlobalStartupFragment())
             }
-            is StartedSettingUp -> {
+            is StartedSettingUp -> if (currentDestId != R.id.qrCodeFragment &&
+                currentDestId != R.id.qrCodeLinkFragment
+            ) {
                 supportActionBar?.show()
-                if (nav.currentDestination?.id != R.id.qrCodeFragment &&
-                    nav.currentDestination?.id != R.id.qrCodeLinkFragment
-                ) nav.navigate(actionGlobalQrCodeFragment())
+                nav.navigate(actionGlobalQrCodeFragment())
             }
-            StartedSetupComplete -> {
-                if (nav.currentDestination?.id == R.id.qrCodeFragment) {
-                    supportActionBar?.hide()
-                    nav.navigate(actionGlobalSetupCompleteFragment())
-                } else if (nav.currentDestination?.id != R.id.statusFragment &&
-                    nav.currentDestination?.id != R.id.setupCompleteFragment
-                ) {
-                    supportActionBar?.show()
-                    nav.navigate(actionGlobalStatusFragment())
-                }
+            StartedSetupComplete -> if (currentDestId == R.id.qrCodeFragment) {
+                supportActionBar?.hide()
+                nav.navigate(actionGlobalSetupCompleteFragment())
+            } else if (currentDestId != R.id.statusFragment &&
+                currentDestId != R.id.setupCompleteFragment
+            ) {
+                supportActionBar?.show()
+                nav.navigate(actionGlobalStatusFragment())
             }
-            ErrorNoNetwork -> {
+            ErrorNoNetwork -> if (currentDestId != R.id.noNetworkFragment) {
                 supportActionBar?.hide()
-                if (nav.currentDestination?.id != R.id.noNetworkFragment)
-                    nav.navigate(actionGlobalNoNetworkFragment())
+                nav.navigate(actionGlobalNoNetworkFragment())
             }
-            ErrorClockSkew -> {
+            ErrorClockSkew -> if (currentDestId != R.id.clockSkewFragment) {
                 supportActionBar?.hide()
-                if (nav.currentDestination?.id != R.id.clockSkewFragment)
-                    nav.navigate(actionGlobalClockSkewFragment())
+                nav.navigate(actionGlobalClockSkewFragment())
             }
-            Stopping -> {
+            Stopping -> if (currentDestId != R.id.stoppingFragment) {
                 supportActionBar?.hide()
-                if (nav.currentDestination?.id != R.id.stoppingFragment)
-                    nav.navigate(actionGlobalStoppingFragment())
+                nav.navigate(actionGlobalStoppingFragment())
             }
-            Wiping -> {
+            Wiping -> if (nav.currentDestination?.id != R.id.wipingFragment) {
                 supportActionBar?.hide()
-                if (nav.currentDestination?.id != R.id.wipingFragment)
-                    nav.navigate(actionGlobalWipingFragment())
+                nav.navigate(actionGlobalWipingFragment())
             }
             Stopped -> {} // nothing to do but needs to be exhaustive for Kotlin 1.7
         }
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/settings/SettingsActivity.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/settings/SettingsActivity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b81c7bf49687ee665a9c909fa834d35850c252bc
--- /dev/null
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/settings/SettingsActivity.kt
@@ -0,0 +1,41 @@
+package org.briarproject.mailbox.android.ui.settings
+
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import androidx.annotation.CallSuper
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.MenuProvider
+import androidx.lifecycle.Lifecycle.State.RESUMED
+import dagger.hilt.android.AndroidEntryPoint
+import org.briarproject.mailbox.databinding.ActivitySettingsBinding
+
+@AndroidEntryPoint
+class SettingsActivity : AppCompatActivity(), MenuProvider {
+
+    private lateinit var binding: ActivitySettingsBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        binding = ActivitySettingsBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        // only needed for up/back navigation in action bar
+        addMenuProvider(this, this, RESUMED)
+        supportActionBar?.setDisplayHomeAsUpEnabled(true)
+    }
+
+    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+    }
+
+    @CallSuper
+    override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+        if (menuItem.itemId == android.R.id.home) {
+            onBackPressedDispatcher.onBackPressed()
+            return true
+        }
+        return false
+    }
+}
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/settings/SettingsFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/settings/SettingsFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8f773c762abe9050c8ad889fa639b31ffd8f42df
--- /dev/null
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/settings/SettingsFragment.kt
@@ -0,0 +1,166 @@
+package org.briarproject.mailbox.android.ui.settings
+
+import android.os.Bundle
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.SwitchPreferenceCompat
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.briarproject.mailbox.R
+import org.briarproject.mailbox.android.ui.MailboxViewModel
+import org.briarproject.mailbox.core.settings.SettingsManager
+import org.briarproject.mailbox.core.system.LocationUtils
+import org.briarproject.mailbox.core.tor.NetworkManager
+import org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_AUTO
+import org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_AUTO_DEFAULT
+import org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_USE
+import org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_USE_MEEK
+import org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_USE_OBFS4
+import org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_USE_OBFS4_DEFAULT
+import org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_USE_SNOWFLAKE
+import org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_USE_VANILLA
+import org.briarproject.mailbox.core.tor.TorPlugin
+import org.briarproject.onionwrapper.CircumventionProvider
+import org.briarproject.onionwrapper.CircumventionProvider.BridgeType.DEFAULT_OBFS4
+import org.briarproject.onionwrapper.CircumventionProvider.BridgeType.MEEK
+import org.briarproject.onionwrapper.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4
+import org.briarproject.onionwrapper.CircumventionProvider.BridgeType.SNOWFLAKE
+import org.briarproject.onionwrapper.CircumventionProvider.BridgeType.VANILLA
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class SettingsFragment : PreferenceFragmentCompat() {
+
+    private val viewModel: MailboxViewModel by activityViewModels()
+
+    private lateinit var autoPref: SwitchPreferenceCompat
+    private lateinit var usePref: SwitchPreferenceCompat
+    private lateinit var snowflakePref: SwitchPreferenceCompat
+    private lateinit var meekPref: SwitchPreferenceCompat
+    private lateinit var obfs4Pref: SwitchPreferenceCompat
+    private lateinit var obfs4DefaultPref: SwitchPreferenceCompat
+    private lateinit var vanillaPref: SwitchPreferenceCompat
+    private lateinit var brideTypePrefs: List<Preference>
+    private lateinit var bridgeTypesCategory: PreferenceCategory
+
+    @Inject
+    lateinit var settingsManager: SettingsManager
+
+    @Inject
+    lateinit var torSettingsStore: TorSettingsStore
+
+    @Inject
+    lateinit var torPlugin: TorPlugin
+
+    @Inject
+    lateinit var locationUtils: LocationUtils
+
+    @Inject
+    lateinit var circumventionProvider: CircumventionProvider
+
+    @Inject
+    lateinit var networkManager: NetworkManager
+
+    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+        setPreferencesFromResource(R.xml.preferences, rootKey)
+        torSettingsStore.setOnSettingsLoadedCallback { settings ->
+            lifecycleScope.launch(Dispatchers.Main) {
+                onAutoChanged(settings.getBoolean(BRIDGE_AUTO, BRIDGE_AUTO_DEFAULT))
+                // we set the store for persistence of settings only after setting up auto value
+                preferenceManager.preferenceDataStore = torSettingsStore
+            }
+        }
+        autoPref = findPreference(BRIDGE_AUTO)!!
+        usePref = findPreference(BRIDGE_USE)!!
+        snowflakePref = findPreference(BRIDGE_USE_SNOWFLAKE)!!
+        meekPref = findPreference(BRIDGE_USE_MEEK)!!
+        obfs4Pref = findPreference(BRIDGE_USE_OBFS4)!!
+        obfs4DefaultPref = findPreference(BRIDGE_USE_OBFS4_DEFAULT)!!
+        vanillaPref = findPreference(BRIDGE_USE_VANILLA)!!
+        brideTypePrefs = listOf(
+            snowflakePref, meekPref, obfs4Pref, obfs4DefaultPref, vanillaPref
+        )
+        bridgeTypesCategory = findPreference("bridgeTypesCategory")!!
+        autoPref.setOnPreferenceChangeListener { _, newValue ->
+            onAutoChanged(newValue as Boolean)
+            true
+        }
+        usePref.setOnPreferenceChangeListener { _, newValue ->
+            onUseBridgesChanged(newValue as Boolean)
+            true
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        // apply settings only when user is leaving settings to prevent Tor changes on each toggle
+        viewModel.onSettingsChanged()
+    }
+
+    private fun onAutoChanged(auto: Boolean) {
+        autoPref.isChecked = auto
+        val country = locationUtils.currentCountry
+        val doBridgesWork = circumventionProvider.doBridgesWork(country)
+        // if automatic mode is on, we show what Tor is using, otherwise we show what user has set
+        if (auto) {
+            setIsPersistent(false)
+            usePref.isChecked = doBridgesWork
+            onUseBridgesChanged(doBridgesWork)
+            val autoTypes = if (networkManager.networkStatus.isIpv6Only) {
+                listOf(MEEK, SNOWFLAKE)
+            } else {
+                circumventionProvider.getSuitableBridgeTypes(country)
+            }
+            snowflakePref.isChecked = autoTypes.contains(SNOWFLAKE)
+            meekPref.isChecked = autoTypes.contains(MEEK)
+            obfs4Pref.isChecked = autoTypes.contains(NON_DEFAULT_OBFS4)
+            obfs4DefaultPref.isChecked = autoTypes.contains(DEFAULT_OBFS4)
+            vanillaPref.isChecked = autoTypes.contains(VANILLA)
+        } else {
+            setIsPersistent(true)
+            val useBridges =
+                torSettingsStore.getBooleanAndStoreDefault(usePref, BRIDGE_USE, doBridgesWork)
+            onUseBridgesChanged(useBridges)
+            val customTypes = torPlugin.customBridgeTypes
+            torSettingsStore.getBooleanAndStoreDefault(
+                pref = snowflakePref,
+                key = BRIDGE_USE_SNOWFLAKE,
+                defaultValue = customTypes.contains(SNOWFLAKE)
+            )
+            torSettingsStore.getBooleanAndStoreDefault(
+                pref = meekPref,
+                key = BRIDGE_USE_MEEK,
+                defaultValue = customTypes.contains(MEEK),
+            )
+            torSettingsStore.getBooleanAndStoreDefault(
+                pref = obfs4Pref,
+                key = BRIDGE_USE_OBFS4,
+                defaultValue = customTypes.contains(NON_DEFAULT_OBFS4),
+            )
+            torSettingsStore.getBooleanAndStoreDefault(
+                pref = obfs4DefaultPref,
+                key = BRIDGE_USE_OBFS4_DEFAULT,
+                defaultValue = customTypes.contains(DEFAULT_OBFS4),
+            )
+            torSettingsStore.getBooleanAndStoreDefault(
+                pref = vanillaPref,
+                key = BRIDGE_USE_VANILLA,
+                defaultValue = customTypes.contains(VANILLA),
+            )
+        }
+    }
+
+    private fun onUseBridgesChanged(useBridges: Boolean) {
+        brideTypePrefs.forEach { it.isVisible = useBridges }
+        bridgeTypesCategory.isVisible = useBridges
+    }
+
+    private fun setIsPersistent(enable: Boolean) {
+        usePref.isPersistent = enable
+        brideTypePrefs.forEach { pref -> pref.isPersistent = enable }
+    }
+}
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/settings/SettingsStore.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/settings/SettingsStore.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e8f608a88247806c31d75dfd917828579ba086a6
--- /dev/null
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/settings/SettingsStore.kt
@@ -0,0 +1,94 @@
+package org.briarproject.mailbox.android.ui.settings
+
+import androidx.core.util.Consumer
+import androidx.preference.PreferenceDataStore
+import androidx.preference.TwoStatePreference
+import org.briarproject.mailbox.core.db.DbException
+import org.briarproject.mailbox.core.lifecycle.IoExecutor
+import org.briarproject.mailbox.core.settings.Settings
+import org.briarproject.mailbox.core.settings.SettingsManager
+import org.briarproject.mailbox.core.tor.TorConstants.SETTINGS_NAMESPACE
+import org.briarproject.mailbox.core.util.LogUtils.info
+import org.briarproject.mailbox.core.util.LogUtils.logDuration
+import org.briarproject.mailbox.core.util.LogUtils.logException
+import org.briarproject.mailbox.core.util.LogUtils.now
+import org.slf4j.LoggerFactory.getLogger
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import javax.inject.Singleton
+
+private val LOG = getLogger(SettingsStore::class.java)
+
+@Singleton
+class TorSettingsStore @Inject constructor(
+    settingsManager: SettingsManager,
+    @IoExecutor
+    ioExecutor: Executor,
+) : SettingsStore(settingsManager, ioExecutor, SETTINGS_NAMESPACE) {
+    fun getBooleanAndStoreDefault(
+        pref: TwoStatePreference,
+        key: String,
+        defaultValue: Boolean,
+    ): Boolean {
+        val s = requireNotNull(settings)
+        val value = if (s.containsKey(key)) {
+            s.getBoolean(key, defaultValue)
+        } else {
+            putBoolean(key, defaultValue)
+            defaultValue
+        }
+        pref.isChecked = value
+        return value
+    }
+}
+
+/**
+ * This is only for storing settings. We still need to retrieve the current value ourselves.
+ */
+open class SettingsStore(
+    private val settingsManager: SettingsManager,
+    private val dbExecutor: Executor,
+    private val namespace: String,
+) : PreferenceDataStore() {
+
+    @Volatile
+    protected var settings: Settings? = null
+    private var callback: Consumer<Settings>? = null
+
+    init {
+        dbExecutor.execute {
+            settings = settingsManager.getSettings(namespace)
+            callback?.accept(settings)
+        }
+    }
+
+    fun setOnSettingsLoadedCallback(callback: Consumer<Settings>) {
+        this.callback = callback
+        if (settings != null) callback.accept(settings)
+    }
+
+    override fun putBoolean(key: String, value: Boolean) {
+        LOG.info { "Store bool setting: $key=$value" }
+        val s = Settings().apply {
+            putBoolean(key, value)
+        }
+        settings!!.putBoolean(key, value)
+        storeSettings(s)
+    }
+
+    override fun getBoolean(key: String, defaultValue: Boolean): Boolean {
+        return settings!!.getBoolean(key, defaultValue)
+    }
+
+    private fun storeSettings(s: Settings) {
+        dbExecutor.execute {
+            try {
+                val start: Long = now()
+                settingsManager.mergeSettings(s, namespace)
+                logDuration(LOG, start) { "Merging $namespace settings" }
+            } catch (e: DbException) {
+                logException(LOG, e, "Error storing settings: ")
+            }
+        }
+    }
+}
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/QrCodeFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/setup/QrCodeFragment.kt
similarity index 94%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/QrCodeFragment.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/setup/QrCodeFragment.kt
index db5a5893b04454565b44a662e9a5f50e13f8fec0..b947a9ba94c04d267514e37be0732f29531e32a4 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/QrCodeFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/setup/QrCodeFragment.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.setup
 
 import android.os.Bundle
 import android.view.LayoutInflater
@@ -37,6 +37,8 @@ import dagger.hilt.android.AndroidEntryPoint
 import org.briarproject.mailbox.R
 import org.briarproject.mailbox.android.StatusManager.MailboxAppState
 import org.briarproject.mailbox.android.StatusManager.StartedSettingUp
+import org.briarproject.mailbox.android.ui.MailboxViewModel
+import org.briarproject.mailbox.android.ui.launchAndRepeatWhileStarted
 
 @AndroidEntryPoint
 class QrCodeFragment : Fragment(), MenuProvider {
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/QrCodeLinkFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/setup/QrCodeLinkFragment.kt
similarity index 82%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/QrCodeLinkFragment.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/setup/QrCodeLinkFragment.kt
index 652356162267247c22ba8a0027e13b73d594428a..cd421ebda40626c4790ef13987583004d8fce130 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/QrCodeLinkFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/setup/QrCodeLinkFragment.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.setup
 
 import android.content.ActivityNotFoundException
 import android.content.ClipData
@@ -28,9 +28,6 @@ import android.content.Intent.EXTRA_TEXT
 import android.os.Build.VERSION.SDK_INT
 import android.os.Bundle
 import android.view.LayoutInflater
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.MenuItem
 import android.view.View
 import android.view.ViewGroup
 import android.widget.TextView
@@ -38,18 +35,18 @@ import android.widget.Toast
 import android.widget.Toast.LENGTH_LONG
 import android.widget.Toast.LENGTH_SHORT
 import androidx.core.content.ContextCompat.getSystemService
-import androidx.core.view.MenuProvider
-import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.Lifecycle
 import com.google.android.material.button.MaterialButton
 import dagger.hilt.android.AndroidEntryPoint
 import org.briarproject.mailbox.R
 import org.briarproject.mailbox.android.StatusManager.MailboxAppState
 import org.briarproject.mailbox.android.StatusManager.StartedSettingUp
+import org.briarproject.mailbox.android.ui.BackFragment
+import org.briarproject.mailbox.android.ui.MailboxViewModel
+import org.briarproject.mailbox.android.ui.launchAndRepeatWhileStarted
 
 @AndroidEntryPoint
-class QrCodeLinkFragment : Fragment(), MenuProvider {
+class QrCodeLinkFragment : BackFragment() {
 
     private val viewModel: MailboxViewModel by activityViewModels()
     private lateinit var linkView: TextView
@@ -65,29 +62,16 @@ class QrCodeLinkFragment : Fragment(), MenuProvider {
     }
 
     override fun onViewCreated(v: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(v, savedInstanceState)
         linkView = v.findViewById(R.id.linkView)
         shareButton = v.findViewById(R.id.shareButton)
         copyButton = v.findViewById(R.id.copyButton)
 
-        // only needed for up/back navigation in action bar
-        requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
-
         launchAndRepeatWhileStarted {
             viewModel.appState.collect { onAppStateChanged(it) }
         }
     }
 
-    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
-    }
-
-    override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
-        if (menuItem.itemId == android.R.id.home) {
-            requireActivity().onBackPressedDispatcher.onBackPressed()
-            return true
-        }
-        return false
-    }
-
     private fun onAppStateChanged(state: MailboxAppState) {
         if (state is StartedSettingUp) {
             linkView.text = state.link
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/SetupCompleteFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/setup/SetupCompleteFragment.kt
similarity index 97%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/SetupCompleteFragment.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/setup/SetupCompleteFragment.kt
index 54d420512eda53a2a2557263d1b2859e2f0c8673..19fa4f38beadf81e2ecb56ffbf6d67c469465944 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/SetupCompleteFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/setup/SetupCompleteFragment.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.setup
 
 import android.os.Bundle
 import android.view.LayoutInflater
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/InitFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/InitFragment.kt
similarity index 97%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/InitFragment.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/InitFragment.kt
index 0c6962e14f99e649f75ceca6458fc478d3dbb93c..ee8496180ff042e681493f9d6bcae0250db87682 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/InitFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/InitFragment.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.startup
 
 import android.os.Bundle
 import android.view.LayoutInflater
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingContainerFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/OnboardingContainerFragment.kt
similarity index 96%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingContainerFragment.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/OnboardingContainerFragment.kt
index 67ebc5dabd2e86b20f458ba29084e176e4b33f40..8d05855cba28000791a971fed06a1d313e450cf2 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingContainerFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/OnboardingContainerFragment.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.startup
 
 import android.os.Bundle
 import android.view.LayoutInflater
@@ -27,6 +27,7 @@ import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
 import androidx.viewpager2.adapter.FragmentStateAdapter
 import androidx.viewpager2.widget.ViewPager2
+import org.briarproject.mailbox.android.ui.MailboxViewModel
 import org.briarproject.mailbox.databinding.FragmentOnboardingContainerBinding
 
 class OnboardingContainerFragment : Fragment() {
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/OnboardingFragment.kt
similarity index 97%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingFragment.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/OnboardingFragment.kt
index ed45b3838f3c3e130a2ddacb26be533f4f2c10b7..140cecd0e58ea001cfa7e25bedfce990d847ae98 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/OnboardingFragment.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.startup
 
 import android.os.Bundle
 import android.view.LayoutInflater
@@ -31,6 +31,7 @@ import androidx.core.widget.ImageViewCompat
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
 import org.briarproject.mailbox.R
+import org.briarproject.mailbox.android.ui.MailboxViewModel
 import org.briarproject.mailbox.databinding.FragmentOnboardingBinding
 
 class Onboarding0Fragment : OnboardingFragment(
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/StartupFailureActivity.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/StartupFailureActivity.kt
similarity index 84%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/StartupFailureActivity.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/StartupFailureActivity.kt
index 866665aa389a75cf929a8cca4e6d83ddaabfc8ad..271c2a780cae969f8bad53c3063e5f9814f2d88d 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/StartupFailureActivity.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/StartupFailureActivity.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.startup
 
 import android.content.Intent
 import android.os.Bundle
@@ -25,9 +25,9 @@ import android.widget.TextView
 import androidx.appcompat.app.AppCompatActivity
 import dagger.hilt.android.AndroidEntryPoint
 import org.briarproject.mailbox.R
-import org.briarproject.mailbox.android.ui.StartupFailureActivity.StartupFailure.CLOCK_ERROR
-import org.briarproject.mailbox.android.ui.StartupFailureActivity.StartupFailure.LIFECYCLE_REUSE
-import org.briarproject.mailbox.android.ui.StartupFailureActivity.StartupFailure.SERVICE_ERROR
+import org.briarproject.mailbox.android.ui.startup.StartupFailureActivity.StartupFailure.CLOCK_ERROR
+import org.briarproject.mailbox.android.ui.startup.StartupFailureActivity.StartupFailure.LIFECYCLE_REUSE
+import org.briarproject.mailbox.android.ui.startup.StartupFailureActivity.StartupFailure.SERVICE_ERROR
 
 @AndroidEntryPoint
 class StartupFailureActivity : AppCompatActivity() {
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/StartupFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/StartupFragment.kt
similarity index 93%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/StartupFragment.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/StartupFragment.kt
index 8dcd70430e1d18e69d59befbb74c9121aa4526b3..5a464f0ec1b35d4e057a9edd10d47d1edcba9f4d 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/StartupFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/startup/StartupFragment.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.startup
 
 import android.os.Bundle
 import android.view.LayoutInflater
@@ -33,6 +33,8 @@ import dagger.hilt.android.AndroidEntryPoint
 import org.briarproject.mailbox.R
 import org.briarproject.mailbox.android.StatusManager.MailboxAppState
 import org.briarproject.mailbox.android.StatusManager.Starting
+import org.briarproject.mailbox.android.ui.MailboxViewModel
+import org.briarproject.mailbox.android.ui.launchAndRepeatWhileStarted
 
 @AndroidEntryPoint
 class StartupFragment : Fragment() {
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/ClockSkewFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/status/ClockSkewFragment.kt
similarity index 96%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/ClockSkewFragment.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/status/ClockSkewFragment.kt
index c3d4189374830cba87bbdc387955b46f21fb724b..40cbe2c7eb4b811cff0bc0ecb321cf62336a03a1 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/ClockSkewFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/status/ClockSkewFragment.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.status
 
 import android.os.Bundle
 import android.view.LayoutInflater
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/NoNetworkFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/status/NoNetworkFragment.kt
similarity index 96%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/NoNetworkFragment.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/status/NoNetworkFragment.kt
index 43f4aaa2c8827651bcf5ed21ee7b601d06aca02c..8e4e48b880469dff990485531dec49506a5280ca 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/NoNetworkFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/status/NoNetworkFragment.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.status
 
 import android.os.Bundle
 import android.view.LayoutInflater
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/StatusFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/status/StatusFragment.kt
similarity index 95%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/StatusFragment.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/status/StatusFragment.kt
index 0c9e215588c31df68145f07c6068fe5528f22f7b..fd36123d49ab874023c2e76da58503edd1124a0c 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/StatusFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/status/StatusFragment.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.status
 
 import android.os.Bundle
 import android.view.LayoutInflater
@@ -32,6 +32,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import dagger.hilt.android.AndroidEntryPoint
 import org.briarproject.mailbox.R
 import org.briarproject.mailbox.android.UiUtils.formatDate
+import org.briarproject.mailbox.android.ui.MailboxViewModel
+import org.briarproject.mailbox.android.ui.launchAndRepeatWhileStarted
 
 @AndroidEntryPoint
 class StatusFragment : Fragment() {
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/StoppingFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/status/StoppingFragment.kt
similarity index 96%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/StoppingFragment.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/status/StoppingFragment.kt
index 92e0d05d80201fd8a14c7f6cdd643cfe959f5622..f72dc0d1f0ae314aa1486f81f6ab6767440b213f 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/StoppingFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/status/StoppingFragment.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.status
 
 import android.os.Bundle
 import android.view.LayoutInflater
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/WipeCompleteActivity.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/wipe/WipeCompleteActivity.kt
similarity index 96%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/WipeCompleteActivity.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/wipe/WipeCompleteActivity.kt
index aa95f8888f299a3de9138e4251cc8be4e253669d..d03549d91f61234771927d59d86a85c9530f3e43 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/WipeCompleteActivity.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/wipe/WipeCompleteActivity.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.wipe
 
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/WipeCompleteFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/wipe/WipeCompleteFragment.kt
similarity index 95%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/WipeCompleteFragment.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/wipe/WipeCompleteFragment.kt
index ce898e8b15114d42a9c9ce25207d8498ca341a1e..58aef8338dfcc68d3d83519bfca950cfd97c9c5d 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/WipeCompleteFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/wipe/WipeCompleteFragment.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.wipe
 
 import android.os.Bundle
 import android.view.LayoutInflater
@@ -29,6 +29,7 @@ import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
 import dagger.hilt.android.AndroidEntryPoint
 import org.briarproject.mailbox.R
+import org.briarproject.mailbox.android.ui.MailboxViewModel
 import org.briarproject.mailbox.core.system.AndroidExecutor
 import org.briarproject.mailbox.core.system.System
 import org.slf4j.LoggerFactory.getLogger
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/WipingFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/wipe/WipingFragment.kt
similarity index 96%
rename from mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/WipingFragment.kt
rename to mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/wipe/WipingFragment.kt
index 885e6b6165197516b3a015481b60a928874cb1a5..e0cc0de593a902dddc9d6bd1746fa3313f9ab2c4 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/WipingFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/wipe/WipingFragment.kt
@@ -17,7 +17,7 @@
  *
  */
 
-package org.briarproject.mailbox.android.ui
+package org.briarproject.mailbox.android.ui.wipe
 
 import android.os.Bundle
 import android.view.LayoutInflater
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt
index 2e3c5c404dd4c0e538867952ef785b5b0083e6b8..2dff2c815c7e368bcfa3e765020465b1a3e1e4ba 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt
@@ -78,9 +78,17 @@ internal class AndroidTorModule {
                     throw UnsupportedOperationException()
                 }
 
+                override fun onSettingsChanged() {
+                    throw UnsupportedOperationException()
+                }
+
                 override fun getHiddenServiceAddress(): String {
                     throw UnsupportedOperationException()
                 }
+
+                override fun getCustomBridgeTypes(): MutableList<CircumventionProvider.BridgeType> {
+                    throw UnsupportedOperationException()
+                }
             }
         }
         return AndroidTorPlugin(
diff --git a/mailbox-android/src/main/res/layout/activity_settings.xml b/mailbox-android/src/main/res/layout/activity_settings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ccd386d2e631617c3e1bb618572ef5773eb09196
--- /dev/null
+++ b/mailbox-android/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/fragmentContainer"
+    android:name="org.briarproject.mailbox.android.ui.settings.SettingsFragment"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".android.ui.settings.SettingsActivity" />
diff --git a/mailbox-android/src/main/res/layout/activity_wipe_complete.xml b/mailbox-android/src/main/res/layout/activity_wipe_complete.xml
index 272c018facf89437f29cd04f17d59097327875e8..cc458d355985b94f168b36a4ef131c4d096008b0 100644
--- a/mailbox-android/src/main/res/layout/activity_wipe_complete.xml
+++ b/mailbox-android/src/main/res/layout/activity_wipe_complete.xml
@@ -2,7 +2,7 @@
 <androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/fragmentContainer"
-    android:name="org.briarproject.mailbox.android.ui.WipeCompleteFragment"
+    android:name="org.briarproject.mailbox.android.ui.wipe.WipeCompleteFragment"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".android.ui.WipeCompleteActivity" />
+    tools:context=".android.ui.wipe.WipeCompleteActivity" />
diff --git a/mailbox-android/src/main/res/menu/main_actions.xml b/mailbox-android/src/main/res/menu/main_actions.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ff816139fee668d8d7e7844eeff36d7194ddce34
--- /dev/null
+++ b/mailbox-android/src/main/res/menu/main_actions.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/action_settings"
+        android:title="@string/prefs_title"
+        app:showAsAction="never" />
+</menu>
diff --git a/mailbox-android/src/main/res/navigation/nav_main.xml b/mailbox-android/src/main/res/navigation/nav_main.xml
index fbb0eee8f501fe1631ceb4be70b7f03dffaae3cf..a727bfb4e76b2217a65114a7929591ec67b80ec6 100644
--- a/mailbox-android/src/main/res/navigation/nav_main.xml
+++ b/mailbox-android/src/main/res/navigation/nav_main.xml
@@ -7,12 +7,12 @@
 
     <fragment
         android:id="@+id/initFragment"
-        android:name="org.briarproject.mailbox.android.ui.InitFragment"
+        android:name="org.briarproject.mailbox.android.ui.startup.InitFragment"
         android:label="InitFragment"
         tools:layout="@layout/fragment_init" />
     <fragment
         android:id="@+id/onboardingFragment"
-        android:name="org.briarproject.mailbox.android.ui.OnboardingContainerFragment"
+        android:name="org.briarproject.mailbox.android.ui.startup.OnboardingContainerFragment"
         android:label="OnboardingContainerFragment"
         tools:layout="@layout/fragment_onboarding" />
     <fragment
@@ -22,12 +22,12 @@
         tools:layout="@layout/fragment_dont_kill_me" />
     <fragment
         android:id="@+id/startupFragment"
-        android:name="org.briarproject.mailbox.android.ui.StartupFragment"
+        android:name="org.briarproject.mailbox.android.ui.startup.StartupFragment"
         android:label="StartupFragment"
         tools:layout="@layout/fragment_startup" />
     <fragment
         android:id="@+id/qrCodeFragment"
-        android:name="org.briarproject.mailbox.android.ui.QrCodeFragment"
+        android:name="org.briarproject.mailbox.android.ui.setup.QrCodeFragment"
         android:label="@string/link_title"
         tools:layout="@layout/fragment_qr">
         <action
@@ -36,37 +36,37 @@
     </fragment>
     <fragment
         android:id="@+id/qrCodeLinkFragment"
-        android:name="org.briarproject.mailbox.android.ui.QrCodeLinkFragment"
+        android:name="org.briarproject.mailbox.android.ui.setup.QrCodeLinkFragment"
         android:label="@string/link_title"
         tools:layout="@layout/fragment_qr_link" />
     <fragment
         android:id="@+id/setupCompleteFragment"
-        android:name="org.briarproject.mailbox.android.ui.SetupCompleteFragment"
+        android:name="org.briarproject.mailbox.android.ui.setup.SetupCompleteFragment"
         android:label="SetupCompleteFragment"
         tools:layout="@layout/fragment_setup_complete" />
     <fragment
         android:id="@+id/statusFragment"
-        android:name="org.briarproject.mailbox.android.ui.StatusFragment"
+        android:name="org.briarproject.mailbox.android.ui.status.StatusFragment"
         android:label="StatusFragment"
         tools:layout="@layout/fragment_status" />
     <fragment
         android:id="@+id/noNetworkFragment"
-        android:name="org.briarproject.mailbox.android.ui.NoNetworkFragment"
+        android:name="org.briarproject.mailbox.android.ui.status.NoNetworkFragment"
         android:label="NoNetworkFragment"
         tools:layout="@layout/fragment_no_network" />
     <fragment
         android:id="@+id/clockSkewFragment"
-        android:name="org.briarproject.mailbox.android.ui.ClockSkewFragment"
+        android:name="org.briarproject.mailbox.android.ui.status.ClockSkewFragment"
         android:label="ClockSkewFragment"
         tools:layout="@layout/fragment_clock_skew" />
     <fragment
         android:id="@+id/stoppingFragment"
-        android:name="org.briarproject.mailbox.android.ui.StoppingFragment"
+        android:name="org.briarproject.mailbox.android.ui.status.StoppingFragment"
         android:label="StoppingFragment"
         tools:layout="@layout/fragment_stopping" />
     <fragment
         android:id="@+id/wipingFragment"
-        android:name="org.briarproject.mailbox.android.ui.WipingFragment"
+        android:name="org.briarproject.mailbox.android.ui.wipe.WipingFragment"
         android:label="WipingFragment"
         tools:layout="@layout/fragment_wiping" />
     <action
diff --git a/mailbox-android/src/main/res/values/strings.xml b/mailbox-android/src/main/res/values/strings.xml
index d6cdf41f190048688dd6d2ebfa62f90e0b0854fb..c6caacd17879006ef68e0de40300ed2e81870f18 100644
--- a/mailbox-android/src/main/res/values/strings.xml
+++ b/mailbox-android/src/main/res/values/strings.xml
@@ -79,4 +79,17 @@
     <string name="startup_failed_lifecycle_reuse">Mailbox has already stopped.\n\nPlease close the app and try again.</string>
     <string name="startup_failed_clock_error">Mailbox was unable to start because your device\'s clock is wrong.\n\nPlease set your device\'s clock to the right time and try again.</string>
 
+    <!-- Preference -->
+    <string name="prefs_title">Settings</string>
+    <string name="prefs_tor_category_title">Tor circumvention settings</string>
+    <string name="prefs_tor_auto_title">Automatic bridge selection</string>
+    <string name="prefs_tor_auto_summary">Bridges are chosen based on your location</string>
+    <string name="prefs_tor_bridges_title">Use bridges</string>
+    <string name="prefs_bridges_category_title">Bridge types</string>
+    <string name="prefs_bridges_snowflake_title" translatable="false">Snowflake</string>
+    <string name="prefs_bridges_vanilla_title" translatable="false">Vanilla</string>
+    <string name="prefs_bridges_obfs4_title" translatable="false">Obfs4</string>
+    <string name="prefs_bridges_obfs_builtin_title">Obfs4 from Tor Browser</string>
+    <string name="prefs_bridges_meek_title" translatable="false">Meek</string>
+
 </resources>
diff --git a/mailbox-android/src/main/res/values/themes.xml b/mailbox-android/src/main/res/values/themes.xml
index 892878df15c459ee8d1946233793b8365691433f..1f8f089329e8268de6d91a5b6444067bef1bd513 100644
--- a/mailbox-android/src/main/res/values/themes.xml
+++ b/mailbox-android/src/main/res/values/themes.xml
@@ -19,4 +19,10 @@
     <style name="Theme.BriarMailbox.Dialog.Destructive" parent="Theme.BriarMailbox.Dialog">
         <item name="buttonBarPositiveButtonStyle">@style/TextButtonDestructive</item>
     </style>
+
+    <style name="Theme.BriarMailbox.NoActionBar" parent="Theme.BriarMailboxBase">
+        <item name="windowActionBar">false</item>
+        <item name="windowNoTitle">true</item>
+    </style>
+
 </resources>
diff --git a/mailbox-android/src/main/res/xml/preferences.xml b/mailbox-android/src/main/res/xml/preferences.xml
new file mode 100644
index 0000000000000000000000000000000000000000..217a859d4f7cc2f5de7db4ed39eeee730dc4a8c3
--- /dev/null
+++ b/mailbox-android/src/main/res/xml/preferences.xml
@@ -0,0 +1,66 @@
+<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <PreferenceCategory
+        app:iconSpaceReserved="false"
+        app:title="@string/prefs_tor_category_title">
+
+        <SwitchPreferenceCompat
+            app:defaultValue="true"
+            app:disableDependentsState="true"
+            app:iconSpaceReserved="false"
+            app:key="bridgeAuto"
+            app:summary="@string/prefs_tor_auto_summary"
+            app:title="@string/prefs_tor_auto_title" />
+
+        <SwitchPreferenceCompat
+            app:defaultValue="false"
+            app:dependency="bridgeAuto"
+            app:iconSpaceReserved="false"
+            app:key="bridgeUse"
+            app:title="@string/prefs_tor_bridges_title" />
+
+    </PreferenceCategory>
+
+    <PreferenceCategory
+        app:key="bridgeTypesCategory"
+        app:iconSpaceReserved="false"
+        app:title="@string/prefs_bridges_category_title">
+
+        <SwitchPreferenceCompat
+            app:defaultValue="false"
+            app:dependency="bridgeUse"
+            app:iconSpaceReserved="false"
+            app:key="bridgeUseSnowflake"
+            app:title="@string/prefs_bridges_snowflake_title" />
+
+        <SwitchPreferenceCompat
+            app:defaultValue="false"
+            app:dependency="bridgeUse"
+            app:iconSpaceReserved="false"
+            app:key="bridgeUseMeek"
+            app:title="@string/prefs_bridges_meek_title" />
+
+        <SwitchPreferenceCompat
+            app:defaultValue="false"
+            app:dependency="bridgeUse"
+            app:iconSpaceReserved="false"
+            app:key="bridgeUseObfs4"
+            app:title="@string/prefs_bridges_obfs4_title" />
+
+        <SwitchPreferenceCompat
+            app:defaultValue="false"
+            app:dependency="bridgeUse"
+            app:iconSpaceReserved="false"
+            app:key="bridgeUseObfs4Default"
+            app:title="@string/prefs_bridges_obfs_builtin_title" />
+
+        <SwitchPreferenceCompat
+            app:defaultValue="false"
+            app:dependency="bridgeUse"
+            app:iconSpaceReserved="false"
+            app:key="bridgeUseVanilla"
+            app:title="@string/prefs_bridges_vanilla_title" />
+
+    </PreferenceCategory>
+
+</PreferenceScreen>
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/AbstractTorPlugin.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/AbstractTorPlugin.java
index 8f2d8d3fbe424103a278af26adb4eb36efaf3d0f..ca56fadcb0d96b66d8772265dd859a87970908e2 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/AbstractTorPlugin.java
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/AbstractTorPlugin.java
@@ -54,14 +54,25 @@ import kotlinx.coroutines.flow.StateFlow;
 import static java.util.Arrays.asList;
 import static java.util.Collections.emptyList;
 import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
+import static org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_AUTO;
+import static org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_AUTO_DEFAULT;
+import static org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_USE;
+import static org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_USE_MEEK;
+import static org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_USE_OBFS4;
+import static org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_USE_OBFS4_DEFAULT;
+import static org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_USE_SNOWFLAKE;
+import static org.briarproject.mailbox.core.tor.TorConstants.BRIDGE_USE_VANILLA;
 import static org.briarproject.mailbox.core.tor.TorConstants.HS_ADDRESS_V3;
 import static org.briarproject.mailbox.core.tor.TorConstants.HS_PRIVATE_KEY_V3;
 import static org.briarproject.mailbox.core.tor.TorConstants.SETTINGS_NAMESPACE;
 import static org.briarproject.mailbox.core.util.LogUtils.info;
 import static org.briarproject.mailbox.core.util.LogUtils.logException;
 import static org.briarproject.mailbox.core.util.PrivacyUtils.scrubOnion;
+import static org.briarproject.onionwrapper.CircumventionProvider.BridgeType.DEFAULT_OBFS4;
 import static org.briarproject.onionwrapper.CircumventionProvider.BridgeType.MEEK;
+import static org.briarproject.onionwrapper.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4;
 import static org.briarproject.onionwrapper.CircumventionProvider.BridgeType.SNOWFLAKE;
+import static org.briarproject.onionwrapper.CircumventionProvider.BridgeType.VANILLA;
 import static org.slf4j.LoggerFactory.getLogger;
 
 public abstract class AbstractTorPlugin implements TorPlugin, EventListener {
@@ -89,6 +100,8 @@ public abstract class AbstractTorPlugin implements TorPlugin, EventListener {
 
 	protected final PluginState state = new PluginState();
 
+	private volatile Settings settings = null;
+
 	AbstractTorPlugin(Executor ioExecutor,
 			SettingsManager settingsManager,
 			NetworkManager networkManager,
@@ -139,6 +152,13 @@ public abstract class AbstractTorPlugin implements TorPlugin, EventListener {
 	@Override
 	public void startService() throws ServiceException {
 		if (used.getAndSet(true)) throw new IllegalStateException();
+		// Load the settings
+		try {
+			settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
+		} catch (DbException e) {
+			logException(LOG, e, "Error while retrieving settings");
+			settings = new Settings();
+		}
 		// Start Tor
 		try {
 			tor.start();
@@ -168,14 +188,7 @@ public abstract class AbstractTorPlugin implements TorPlugin, EventListener {
 	private void publishHiddenService(int port) {
 		if (!tor.isTorRunning()) return;
 
-		Settings s;
-		try {
-			s = settingsManager.getSettings(SETTINGS_NAMESPACE);
-		} catch (DbException e) {
-			logException(LOG, e, "Error while retrieving settings");
-			s = new Settings();
-		}
-		String privateKey3 = s.get(HS_PRIVATE_KEY_V3);
+		String privateKey3 = settings.get(HS_PRIVATE_KEY_V3);
 		createV3HiddenService(port, privateKey3);
 	}
 
@@ -197,6 +210,8 @@ public abstract class AbstractTorPlugin implements TorPlugin, EventListener {
 			s.put(HS_PRIVATE_KEY_V3, hsProps.privKey);
 			try {
 				settingsManager.mergeSettings(s, SETTINGS_NAMESPACE);
+				// update cached settings with merge result
+				settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
 			} catch (DbException e) {
 				logException(LOG, e, "Error while merging settings");
 			}
@@ -204,9 +219,8 @@ public abstract class AbstractTorPlugin implements TorPlugin, EventListener {
 	}
 
 	@Nullable
-	public String getHiddenServiceAddress() throws DbException {
-		Settings s = settingsManager.getSettings(SETTINGS_NAMESPACE);
-		return s.get(HS_ADDRESS_V3);
+	public String getHiddenServiceAddress() {
+		return settings == null ? null : settings.get(HS_ADDRESS_V3);
 	}
 
 	private void enableBridges(List<BridgeType> bridgeTypes, String countryCode)
@@ -240,6 +254,19 @@ public abstract class AbstractTorPlugin implements TorPlugin, EventListener {
 		}
 	}
 
+	@Override
+	public void onSettingsChanged() {
+		ioExecutor.execute(() -> {
+			try {
+				settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
+			} catch (DbException e) {
+				logException(LOG, e, "Error while retrieving settings");
+				settings = new Settings();
+			}
+			updateConnectionStatus(networkManager.getNetworkStatus());
+		});
+	}
+
 	private void updateConnectionStatus(NetworkStatus status) {
 		connectionStatusExecutor.execute(() -> {
 			if (!tor.isTorRunning()) return;
@@ -247,7 +274,6 @@ public abstract class AbstractTorPlugin implements TorPlugin, EventListener {
 			boolean wifi = status.isWifi();
 			boolean ipv6Only = status.isIpv6Only();
 			String country = locationUtils.getCurrentCountry();
-			boolean bridgesWork = circumventionProvider.doBridgesWork(country);
 
 			if (LOG.isInfoEnabled()) {
 				LOG.info("Online: " + online + ", wifi: " + wifi
@@ -264,19 +290,7 @@ public abstract class AbstractTorPlugin implements TorPlugin, EventListener {
 			} else {
 				LOG.info("Enabling network");
 				enableNetwork = true;
-				if (bridgesWork) {
-					if (ipv6Only) {
-						bridgeTypes = asList(MEEK, SNOWFLAKE);
-					} else {
-						bridgeTypes = circumventionProvider
-								.getSuitableBridgeTypes(country);
-					}
-					if (LOG.isInfoEnabled()) {
-						LOG.info("Using bridge types " + bridgeTypes);
-					}
-				} else {
-					LOG.info("Not using bridges");
-				}
+				bridgeTypes = getBridgeTypes(country, ipv6Only);
 				if (wifi) {
 					LOG.info("Enabling connection padding");
 					enableConnectionPadding = true;
@@ -298,6 +312,78 @@ public abstract class AbstractTorPlugin implements TorPlugin, EventListener {
 		});
 	}
 
+	@Override
+	public List<BridgeType> getCustomBridgeTypes() {
+		return getCustomBridgeTypes(locationUtils.getCurrentCountry(),
+				networkManager.getNetworkStatus().isIpv6Only());
+	}
+
+	private List<BridgeType> getCustomBridgeTypes(String country,
+			boolean ipv6Only) {
+		List<BridgeType> defaultTypes;
+		if (ipv6Only) {
+			defaultTypes = asList(MEEK, SNOWFLAKE);
+		} else {
+			defaultTypes =
+					circumventionProvider.getSuitableBridgeTypes(country);
+		}
+		ArrayList<BridgeType> types = new ArrayList<>();
+		if (settings.getBoolean(BRIDGE_USE_SNOWFLAKE,
+				defaultTypes.contains(SNOWFLAKE))) {
+			types.add(SNOWFLAKE);
+		}
+		if (settings.getBoolean(BRIDGE_USE_MEEK, defaultTypes.contains(MEEK))) {
+			types.add(MEEK);
+		}
+		if (settings.getBoolean(BRIDGE_USE_OBFS4,
+				defaultTypes.contains(NON_DEFAULT_OBFS4))) {
+			types.add(NON_DEFAULT_OBFS4);
+		}
+		if (settings.getBoolean(BRIDGE_USE_OBFS4_DEFAULT,
+				defaultTypes.contains(DEFAULT_OBFS4))) {
+			types.add(DEFAULT_OBFS4);
+		}
+		if (settings.getBoolean(BRIDGE_USE_VANILLA,
+				defaultTypes.contains(VANILLA))) {
+			types.add(VANILLA);
+		}
+		return types;
+	}
+
+	private List<BridgeType> getBridgeTypes(String country, boolean ipv6Only) {
+		List<BridgeType> bridgeTypes = emptyList();
+		boolean bridgesNeeded =
+				circumventionProvider.doBridgesWork(country);
+		boolean bridgeAuto =
+				settings.getBoolean(BRIDGE_AUTO, BRIDGE_AUTO_DEFAULT);
+		if (bridgeAuto) {
+			if (bridgesNeeded) {
+				if (ipv6Only) {
+					bridgeTypes = asList(MEEK, SNOWFLAKE);
+				} else {
+					bridgeTypes = circumventionProvider
+							.getSuitableBridgeTypes(country);
+				}
+				if (LOG.isInfoEnabled()) {
+					LOG.info("Using bridge types " + bridgeTypes);
+				}
+			} else {
+				LOG.info("Not using bridges");
+			}
+		} else {
+			boolean useBridges = settings.getBoolean(BRIDGE_USE, bridgesNeeded);
+			if (useBridges) {
+				bridgeTypes = getCustomBridgeTypes(country, ipv6Only);
+				if (LOG.isInfoEnabled()) {
+					LOG.info("Using bridge types " + bridgeTypes);
+				}
+			} else {
+				LOG.info("Not using bridges");
+			}
+		}
+		return bridgeTypes;
+	}
+
 	@ThreadSafe
 	private class PluginState {
 
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorConstants.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorConstants.java
index ca6b6633ecfe7eb554e85c5016f63bf91271f57d..0079982fc3cd972792cfdc4ad06f807fb17d950e 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorConstants.java
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorConstants.java
@@ -25,6 +25,24 @@ public interface TorConstants {
 	String SETTINGS_NAMESPACE = "Tor";
 	String HS_PRIVATE_KEY_V3 = "onionPrivKey3";
 	String HS_ADDRESS_V3 = "onionAddress3";
+	/**
+	 * Whether circumvention bridge handling should be handled automatically.
+	 */
+	String BRIDGE_AUTO = "bridgeAuto";
+	/**
+	 * Whether automatic circumvention handling is enabled by default or not.
+	 */
+	boolean BRIDGE_AUTO_DEFAULT = true;
+	/**
+	 * Whether bridges should be used for circumvention.
+	 * Only consider when {@link #BRIDGE_AUTO} is false.
+	 */
+	String BRIDGE_USE = "bridgeUse";
+	String BRIDGE_USE_SNOWFLAKE = "bridgeUseSnowflake";
+	String BRIDGE_USE_MEEK = "bridgeUseMeek";
+	String BRIDGE_USE_OBFS4 = "bridgeUseObfs4";
+	String BRIDGE_USE_OBFS4_DEFAULT = "bridgeUseObfs4Default";
+	String BRIDGE_USE_VANILLA = "bridgeUseVanilla";
 
 	int SOCKS_PORT = 59054;
 	int CONTROL_PORT = 59055;
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
index bbc28f42779005a40bd06206cb6c34c8939a2c85..39c907e5d6e9b4a5a6406eb9d90b607f566ef49a 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
@@ -2,13 +2,38 @@ package org.briarproject.mailbox.core.tor;
 
 import org.briarproject.mailbox.core.db.DbException;
 import org.briarproject.mailbox.core.lifecycle.Service;
+import org.briarproject.mailbox.core.settings.Settings;
+import org.briarproject.nullsafety.NotNullByDefault;
+import org.briarproject.onionwrapper.CircumventionProvider.BridgeType;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
 
 import kotlinx.coroutines.flow.StateFlow;
 
+@NotNullByDefault
 public interface TorPlugin extends Service {
 
 	StateFlow<TorPluginState> getState();
 
+	/**
+	 * Call this whenever {@link Settings} in
+	 * {@link TorConstants#SETTINGS_NAMESPACE} have changed.
+	 */
+	void onSettingsChanged();
+
+	/**
+	 * This is only available after {@link #startService()} has returned.
+	 * Otherwise returns null.
+	 */
+	@Nullable
 	String getHiddenServiceAddress() throws DbException;
 
+	/**
+	 * Get a list of bridge types that Tor will be using with current settings,
+	 * country and {@link NetworkStatus}.
+	 */
+	List<BridgeType> getCustomBridgeTypes();
+
 }
diff --git a/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/FakeTorPlugin.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/FakeTorPlugin.kt
index d56ec5cfb1c9207dba95387effb3d229e71b87fc..5d387bb8c7ecff90b3b8cdc21b52362c27aaa014 100644
--- a/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/FakeTorPlugin.kt
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/FakeTorPlugin.kt
@@ -2,6 +2,7 @@ package org.briarproject.mailbox.core.tor
 
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
+import org.briarproject.onionwrapper.CircumventionProvider.BridgeType
 import javax.inject.Inject
 
 class FakeTorPlugin @Inject constructor() : TorPlugin {
@@ -20,5 +21,9 @@ class FakeTorPlugin @Inject constructor() : TorPlugin {
         return state
     }
 
+    override fun onSettingsChanged() {
+    }
+
     override fun getHiddenServiceAddress(): String? = null
+    override fun getCustomBridgeTypes(): List<BridgeType> = emptyList()
 }