Commit 977ec86c authored by Matthew Finkel's avatar Matthew Finkel Committed by Pier Angelo Vendrame
Browse files

TB 40041 [android]: Implement Tor Network Settings

Originally, fenix#40041.
parent acaff875
Loading
Loading
Loading
Loading
+62 −7
Original line number Diff line number Diff line
@@ -13,6 +13,7 @@ import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
@@ -53,6 +54,7 @@ import org.mozilla.fenix.GleanMetrics.TrackingProtection
import org.mozilla.fenix.GleanMetrics.Translations
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ReleaseChannel
import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint
import org.mozilla.fenix.databinding.AmoCollectionOverrideDialogBinding
import org.mozilla.fenix.ext.application
@@ -63,13 +65,18 @@ import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.gecko.GeckoProvider
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.perf.ProfilerViewModel
import org.mozilla.fenix.settings.account.AccountUiView
import org.mozilla.fenix.snackbar.FenixSnackbarDelegate
import org.mozilla.fenix.snackbar.SnackbarBinding
import org.mozilla.fenix.tor.QuickStartPreference
import org.mozilla.fenix.tor.SecurityLevel
import org.mozilla.fenix.tor.TorBridgeTransportConfig
import org.mozilla.fenix.tor.TorEvents
import org.mozilla.fenix.utils.Settings
import org.mozilla.geckoview.BuildConfig
import kotlin.system.exitProcess
import org.mozilla.fenix.GleanMetrics.Settings as SettingsMetrics

@@ -174,6 +181,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
    }

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

@@ -214,7 +223,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
        update(shouldUpdateAccountUIState = !creatingFragment)

        requireView().findViewById<RecyclerView>(R.id.recycler_view)
            ?.hideInitialScrollBar(viewLifecycleOwner.lifecycleScope)
            .also {
                it?.hideInitialScrollBar(viewLifecycleOwner.lifecycleScope)
                // Prevent disabled settings from having a collapsing animation on open
                it?.disableHidingAnimation()
            }

        args.preferenceToScrollTo?.let {
            scrollToPreference(it)
@@ -274,9 +287,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
//            getString(R.string.preferences_credit_cards_2)
//        }

        val openLinksInAppsSettingsPreference =
            requirePreference<Preference>(R.string.pref_key_open_links_in_apps)
        openLinksInAppsSettingsPreference.summary = context?.settings()?.getOpenLinksInAppsString()
//        val openLinksInAppsSettingsPreference =
//            requirePreference<Preference>(R.string.pref_key_open_links_in_apps)
//        openLinksInAppsSettingsPreference.summary = context?.settings()?.getOpenLinksInAppsString()

        // Hide "Delete browsing data on quit" when in Private Browsing-only mode
        deleteBrowsingDataPreference.isVisible =
@@ -453,9 +466,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
                null
            }

            resources.getString(R.string.pref_key_open_links_in_apps) -> {
                SettingsFragmentDirections.actionSettingsFragmentToOpenLinksInAppsFragment()
            }
//            resources.getString(R.string.pref_key_open_links_in_apps) -> {
//                SettingsFragmentDirections.actionSettingsFragmentToOpenLinksInAppsFragment()
//            }

            resources.getString(R.string.pref_key_sync_debug) -> {
                SettingsFragmentDirections.actionSettingsFragmentToSyncDebugFragment()
@@ -584,6 +597,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
        setupSearchPreference()
        setupHomepagePreference()
        setupTrackingProtectionPreference()
        setupConnectionPreferences()
    }

    /**
@@ -618,6 +632,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
        }
    }

    private fun RecyclerView.disableHidingAnimation() {
        this.setItemAnimator(null)
        this.setLayoutAnimation(null)
    }
    private fun updateFxAAllowDomesticChinaServerMenu() {
        val settings = requireContext().settings()
        val preferenceAllowDomesticChinaServer =
@@ -756,6 +774,43 @@ class SettingsFragment : PreferenceFragmentCompat() {
        }
    }

    internal fun setupConnectionPreferences() {
        // will be needed for phase2
        //val torController = requireContext().components.torController

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

        requirePreference<QuickStartPreference>(R.string.pref_key_quick_start).apply {
            setOnPreferenceClickListener {
                context.components.torController.quickstart = !context.components.torController.quickstart
                updateSwitch()
                true
            }
        }

        requirePreference<Preference>(R.string.pref_key_use_html_connection_ui).apply {
            onPreferenceChangeListener = object : SharedPreferenceUpdater() {}
            isVisible = Config.channel != ReleaseChannel.Release
        }

        requirePreference<Preference>(R.string.pref_key_tor_logs).apply {
            setOnPreferenceClickListener {
                val directions =
                    SettingsFragmentDirections.actionSettingsFragmentToTorLogsFragment()
                requireView().findNavController().navigate(directions)
                true
            }
        }
    }

    @VisibleForTesting
    internal fun setupCookieBannerPreference() {
        // FxNimbus.features.cookieBanners.recordExposure()
+155 −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())
    }

    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)
                    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)
            )
        }
    }
}
+32 −0
Original line number Diff line number Diff line
package org.mozilla.fenix.tor

import android.content.Context
import android.util.AttributeSet
import androidx.preference.PreferenceViewHolder
import androidx.preference.SwitchPreference
import com.google.android.material.switchmaterial.SwitchMaterial
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components

class QuickStartPreference @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
) : SwitchPreference(context, attrs) {

    private var switchView: SwitchMaterial? = null

    init {
        widgetLayoutResource = R.layout.preference_quick_start
    }

    override fun onBindViewHolder(holder: PreferenceViewHolder) {
        super.onBindViewHolder(holder)
        switchView = holder.findViewById(R.id.switch_widget) as SwitchMaterial

        updateSwitch()
    }

    fun updateSwitch() {
        switchView?.isChecked = context.components.torController.quickstart
    }
}
+138 −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.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.R

class TorLogsComposeFragment : Fragment() {
    private val viewModel: TorLogsViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
        return ComposeView(requireContext()).apply {
            setContent {
                Scaffold(
                    floatingActionButton = { CopyLogsButton() },
                    content = { TorLogs(paddingValues = it) },
                )
            }
        }
    }

    @Composable
    private fun TorLogs(paddingValues: PaddingValues) {
        val torLogsState = remember { mutableStateOf<List<TorLog>>(emptyList()) }
        val lifecycleOwner = LocalLifecycleOwner.current
        val scrollState = rememberScrollState()

        DisposableEffect(viewModel.torLogs(), lifecycleOwner) {
            val observer = Observer<List<TorLog>> { logs ->
                torLogsState.value = logs
            }
            viewModel.torLogs().observe(lifecycleOwner, observer)
            onDispose {
                viewModel.torLogs().removeObserver(observer)
            }
        }

        val torLogs = torLogsState.value

        LaunchedEffect(torLogs) {
            scrollState.animateScrollTo(scrollState.maxValue)
        }

        SelectionContainer {
            Column(
                // Column instead of LazyColumn so that you can select all the logs, and not just one "screen" at a time
                // The logs won't be too big so loading them all instead of just whats visible shouldn't be a big deal
                modifier = Modifier
                    .fillMaxSize()
                    .verticalScroll(scrollState)
                    .padding(paddingValues)
                    .background(PhotonColors.Ink50), // Standard background color
            ) {
                for (log in torLogs) {
                    LogRow(log = log)
                }
            }
        }
    }

    @Composable
    @Stable
    private fun LogRow(log: TorLog, modifier: Modifier = Modifier) {
        Column(
            modifier
                .fillMaxWidth()
                .padding(
                    start = 16.dp,
                    end = 16.dp,
                    bottom = 16.dp,
                ),
        ) {
            Text(
                text = log.timestamp,
                color = PhotonColors.LightGrey40,
                modifier = modifier
                    .padding(bottom = 4.dp),
            )
            Text(
                text = "[${log.type}] " + log.text,
                color = PhotonColors.LightGrey05,
            )
        }
    }

    @Composable
    private fun CopyLogsButton() {
        FloatingActionButton(
            onClick = { viewModel.copyAllLogsToClipboard() },
            content = {
                Icon(
                    painter = painterResource(id = R.drawable.ic_copy),
                    contentDescription = getString(R.string.share_copy_link_to_clipboard),
                )
            },
            backgroundColor = PhotonColors.Violet50, // Same color as connect button
            contentColor = PhotonColors.LightGrey05,
        )
    }
}
+83 −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.app.Application
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components

class TorLogsViewModel(application: Application) : AndroidViewModel(application), TorLogs {
    private val torController = application.components.torController
    private val clipboardManager =
        application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager

    private val _torLogs: MutableLiveData<List<TorLog>> = MutableLiveData(mutableListOf())

    fun torLogs(): LiveData<List<TorLog>> {
        return _torLogs
    }

    private fun addLog(log: TorLog) {
        _torLogs.value = _torLogs.value?.plus(log) ?: return
    }

    init {
        setupClipboardListener()
        torController.registerTorLogListener(this)
        val currentEntries = torController.logEntries
        for (log in currentEntries) {
            addLog(log)
        }
    }

    override fun onLog(type: String?, message: String?, timestamp: String?) {
        addLog(TorLog(type ?: "null", message ?: "null", timestamp ?: "null"))
    }

    override fun onCleared() {
        super.onCleared()
        torController.unregisterTorLogListener(this)
    }

    private fun setupClipboardListener() {
        clipboardManager.addPrimaryClipChangedListener {
            // Only show a toast for Android 12 and lower.
            // https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications
            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
                Toast.makeText(
                    getApplication<Application>().applicationContext,
                    getApplication<Application>().getString(R.string.toast_copy_link_to_clipboard), // "Copied to clipboard" already translated
                    Toast.LENGTH_SHORT,
                ).show()
            }
        }
    }

    fun copyAllLogsToClipboard() {
        clipboardManager.setPrimaryClip(
            ClipData.newPlainText(
                getApplication<Application>().getString(R.string.preferences_tor_logs),
                getAllTorLogs(),
            ),
        )
    }

    private fun getAllTorLogs(): String {
        var ret = ""
        for (log in torLogs().value
            ?: return getApplication<Application>().getString(R.string.default_error_msg)) {
            ret += "${log.timestamp} [${log.type}] ${log.text}\n"
        }
        return ret
    }
}
Loading