Commit e30022b5 authored by Matthew Finkel's avatar Matthew Finkel
Browse files

Bug 40041: Implement Tor Network Settings

parent f94ec7da
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -221,6 +221,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
            resources.getString(R.string.pref_key_search_settings) -> {
                SettingsFragmentDirections.actionSettingsFragmentToSearchEngineFragment()
            }
            resources.getString(R.string.pref_key_tor_network_settings) -> {
                SettingsFragmentDirections.actionSettingsFragmentToTorNetworkSettingsFragment()
            }
            resources.getString(R.string.pref_key_tracking_protection_settings) -> {
                requireContext().metrics.track(Event.TrackingProtectionSettings)
                SettingsFragmentDirections.actionSettingsFragmentToTrackingProtectionFragment()
+162 −0
Original line number Diff line number Diff line
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.settings

import android.os.Bundle
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import org.mozilla.fenix.Config
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.tor.TorBridgeTransportConfig
import org.mozilla.fenix.utils.view.addToRadioGroup
import org.mozilla.fenix.utils.view.GroupableRadioButton
import org.mozilla.fenix.utils.view.uncheckAll

/**
 * Displays the toggle for enabling bridges, options for built-in pluggable transports, and an additional
 * preference for configuring a user-provided bridge.
 */
@Suppress("SpreadOperator")
class TorBridgeConfigFragment : PreferenceFragmentCompat() {
    private val builtinBridgeRadioGroups = mutableListOf<GroupableRadioButton>()
    private var previousTransportConfig: TorBridgeTransportConfig? = null

    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.tor_bridge_config_preferences, rootKey)

        // Initialize radio button group for built-in bridge transport types
        val radioObfs4 = bindBridgeTransportRadio(TorBridgeTransportConfig.BUILTIN_OBFS4)
        val radioMeekAzure = bindBridgeTransportRadio(TorBridgeTransportConfig.BUILTIN_MEEK_AZURE)
        val radioSnowflake = bindBridgeTransportRadio(TorBridgeTransportConfig.BUILTIN_SNOWFLAKE)

        builtinBridgeRadioGroups.addAll(mutableListOf(radioObfs4, radioMeekAzure, radioSnowflake))

        // `*` is Kotlin's "spread" operator, for expanding an Array as a vararg.
        addToRadioGroup(*builtinBridgeRadioGroups.toTypedArray())

        // Snowflake is not available on Release at this time.
        if (Config.channel.isRelease) {
            radioSnowflake.apply {
                isVisible = false
            }
        }
    }

    override fun onResume() {
        super.onResume()

        showToolbar(getString(R.string.preferences_tor_network_settings_bridge_config))

        val bridgesEnabled = requireContext().components.torController.bridgesEnabled

        val prefBridgeConfig =
            requirePreference<SwitchPreference>(R.string.pref_key_tor_network_settings_bridge_config_toggle)
        prefBridgeConfig.apply {
            isChecked = bridgesEnabled
            setOnPreferenceChangeListener<Boolean> { preference, enabled ->
                preference.context.components.torController.bridgesEnabled = enabled
                updateCurrentConfiguredBridgePref(preference)
                preference.context.components.torController.restartTor()
                true
            }
        }

        val userProvidedBridges = requirePreference<EditTextPreference>(
            R.string.pref_key_tor_network_settings_bridge_config_user_provided_bridge
        )
        userProvidedBridges.apply {
            setOnPreferenceChangeListener<String> { preference, userProvidedBridge ->
                builtinBridgeRadioGroups.uncheckAll()

                preference.context.components.torController.bridgeTransport = TorBridgeTransportConfig.USER_PROVIDED
                preference.context.components.torController.userProvidedBridges = userProvidedBridge
                updateCurrentConfiguredBridgePref(preference)
                preference.context.components.torController.restartTor()
                true
            }
            val userProvidedBridge: String? = context.components.torController.userProvidedBridges
            if (userProvidedBridge != null) {
                setText(userProvidedBridge)
            }
        }

        val currentBridgeType = prefBridgeConfig.context.components.torController.bridgeTransport
        // Cache the current configured transport type
        previousTransportConfig = currentBridgeType
        builtinBridgeRadioGroups.uncheckAll()
        if (currentBridgeType != TorBridgeTransportConfig.USER_PROVIDED) {
            val bridgeRadioButton = requirePreference<RadioButtonPreference>(currentBridgeType.preferenceKey)
            bridgeRadioButton.setCheckedWithoutClickListener(true)
        }

        updateCurrentConfiguredBridgePref(prefBridgeConfig)
    }

    private fun bindBridgeTransportRadio(
        bridge: TorBridgeTransportConfig
    ): RadioButtonPreference {
        val radio = requirePreference<RadioButtonPreference>(bridge.preferenceKey)

        radio.apply {
            setOnPreferenceChangeListener<Boolean> { preference, isChecked ->
                if (isChecked && (previousTransportConfig!! != bridge)) {
                    preference.context.components.torController.bridgeTransport = bridge
                    previousTransportConfig = bridge
                    updateCurrentConfiguredBridgePref(preference)
                    context.components.torController.restartTor()
                }
                true
            }
        }

        return radio
    }

    private fun setCurrentBridgeLabel(currentBridgePref: Preference?, bridge: String) {
        currentBridgePref?.apply {
            title = getString(
                R
                .string
                .preferences_tor_network_settings_bridge_config_current_bridge,
                bridge
            )
        }
    }

    private fun updateCurrentConfiguredBridgePref(preference: Preference) {
        val currentBridge: Preference? =
            findPreference(
                getString(
                    R.string.pref_key_tor_network_settings_bridge_config_current_bridge
                )
            )

        val enabled = requireContext().components.torController.bridgesEnabled

        if (enabled) {
            val configuredBridge = preference.context.components.torController.bridgeTransport
            var bridges = when (configuredBridge) {
                TorBridgeTransportConfig.USER_PROVIDED ->
                    preference.context.components.torController.userProvidedBridges
                else -> configuredBridge.transportName
            }

            if (bridges == null) {
                bridges = "not known"
            }
            setCurrentBridgeLabel(currentBridge, bridges)
        } else {
            setCurrentBridgeLabel(
                currentBridge,
                getString(R.string.tor_network_settings_bridge_not_configured)
            )
        }
    }
}
+162 −0
Original line number Diff line number Diff line
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.settings

import android.os.Bundle
import androidx.navigation.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.tor.TorBridgeTransportConfig
import org.mozilla.fenix.tor.TorEvents

/**
 * Lets the user configure Tor network connection settings
 */
class TorNetworkSettingsFragment : PreferenceFragmentCompat(), TorEvents {
    override fun onResume() {
        super.onResume()

        val torController = requireContext().components.torController

        torController.registerTorListener(this)

        showToolbar(getString(R.string.preferences_tor_network_settings))

        val yesString = getString(R.string.preferences_tor_network_settings_yes)
        val noString = getString(R.string.preferences_tor_network_settings_no)

        requirePreference<Preference>(R.string.pref_key_tor_network_settings_bridge_config).apply {
            setOnPreferenceClickListener {
                val directions =
                    TorNetworkSettingsFragmentDirections
                        .actionTorNetworkSettingsFragmentToTorBridgeConfigFragment()
                requireView().findNavController().navigate(directions)
                true
            }

            if (torController.bridgesEnabled) {
                if (torController.bridgeTransport == TorBridgeTransportConfig.USER_PROVIDED) {
                    summary =
                        getString(
                            R
                            .string
                            .preferences_tor_network_settings_bridge_config_description_user_provided_enabled
                        )
                } else {
                    summary =
                        getString(
                            R
                            .string
                            .preferences_tor_network_settings_bridge_config_description_builtin_transport_enabled
                        )
                }
            } else {
                summary =
                    getString(
                        R
                        .string
                        .preferences_tor_network_settings_bridge_config_description
                    )
            }
        }

        requirePreference<Preference>(R.string.pref_key_tor_network_settings_bridges_enabled).apply {
            val formatStringRes = R.string.preferences_tor_network_settings_bridges_enabled
            title = if (torController.bridgesEnabled) {
                getString(formatStringRes, yesString)
            } else {
                getString(formatStringRes, noString)
            }
        }

        setStatus()
    }

    private fun setStatus() {
        val torController = requireContext().components.torController
        val yesString = getString(R.string.preferences_tor_network_settings_yes)
        val noString = getString(R.string.preferences_tor_network_settings_no)

        requirePreference<Preference>(R.string.pref_key_tor_network_settings_tor_ready).apply {
            val formatStringRes = R.string.preferences_tor_network_settings_tor_ready
            @SuppressWarnings("ComplexCondition")
            title = if (!torController.isStarting &&
                torController.isConnected &&
                torController.isBootstrapped &&
                !torController.isRestarting) {
                getString(formatStringRes, yesString)
            } else {
                getString(formatStringRes, noString)
            }
        }

        requirePreference<Preference>(R.string.pref_key_tor_network_settings_state).apply {
            val formatStringRes = R.string.preferences_tor_network_settings_state

            title = if (torController.isRestarting) {
                getString(formatStringRes,
                    getString(
                        R
                        .string
                        .preferences_tor_network_settings_restarting
                    )
                )
            } else if (torController.isStarting) {
                getString(formatStringRes,
                    getString(
                        R
                        .string
                        .preferences_tor_network_settings_connecting
                    )
                )
            } else if (torController.isConnected) {
                getString(formatStringRes,
                    getString(
                        R
                        .string
                        .preferences_tor_network_settings_connected
                    )
                )
            } else {
                getString(formatStringRes,
                    getString(
                        R
                        .string
                        .preferences_tor_network_settings_disconnected
                    )
                )
            }
        }
    }

    override fun onStop() {
        super.onStop()
        requireContext().components.torController.unregisterTorListener(this)
    }

    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.tor_network_settings_preferences, rootKey)
    }

    @SuppressWarnings("EmptyFunctionBlock")
    override fun onTorConnecting() {
    }

    @SuppressWarnings("EmptyFunctionBlock")
    override fun onTorConnected() {
    }

    @SuppressWarnings("EmptyFunctionBlock")
    override fun onTorStopped() {
    }

    override fun onTorStatusUpdate(entry: String?, status: String?) {
        setStatus()
    }
}
+48 −0
Original line number Diff line number Diff line
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.tor

import android.os.Parcelable
import androidx.annotation.StringRes
import kotlinx.android.parcel.Parcelize
import org.mozilla.fenix.R

@Parcelize
enum class TorBridgeTransportConfig(
    @StringRes val preferenceKey: Int,
    val transportName: String
) : Parcelable {

    BUILTIN_OBFS4(
        preferenceKey = R.string.pref_key_tor_network_settings_bridge_config_builtin_bridge_obfs4,
        transportName = "obfs4"
    ),
    BUILTIN_MEEK_AZURE(
        preferenceKey = R.string.pref_key_tor_network_settings_bridge_config_builtin_bridge_meek_azure,
        transportName = "meek"
    ),
    BUILTIN_SNOWFLAKE(
        preferenceKey = R.string.pref_key_tor_network_settings_bridge_config_builtin_bridge_snowflake,
        transportName = "snowflake"
    ),
    USER_PROVIDED(
        preferenceKey = R.string.pref_key_tor_network_settings_bridge_config_user_provided_bridge,
        transportName = "user_provided"
    )
}

object TorBridgeTransportConfigUtil {
    fun getStringToBridgeTransport(bridge: String): TorBridgeTransportConfig {
        return when (bridge) {
            TorBridgeTransportConfig.BUILTIN_OBFS4.transportName ->
                TorBridgeTransportConfig.BUILTIN_OBFS4
            TorBridgeTransportConfig.BUILTIN_MEEK_AZURE.transportName ->
                TorBridgeTransportConfig.BUILTIN_MEEK_AZURE
            TorBridgeTransportConfig.BUILTIN_SNOWFLAKE.transportName ->
                TorBridgeTransportConfig.BUILTIN_SNOWFLAKE
            else -> TorBridgeTransportConfig.USER_PROVIDED
        }
    }
}
+9 −0
Original line number Diff line number Diff line
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="18dp"
    android:height="18dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
  <path
      android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z"
      android:fillColor="#ffffff"/>
</vector>
Loading