Commit adde3ef2 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 ac887fbf
Loading
Loading
Loading
Loading
+76 −8
Original line number Diff line number Diff line
@@ -31,8 +31,10 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.sync.AccountObserver
@@ -52,6 +54,7 @@ import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.TrackingProtection
import org.mozilla.fenix.GleanMetrics.Translations
import org.mozilla.fenix.R
import org.mozilla.fenix.ReleaseChannel
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint
import org.mozilla.fenix.databinding.AmoCollectionOverrideDialogBinding
@@ -70,6 +73,7 @@ import org.mozilla.fenix.settings.account.AccountUiView
import org.mozilla.fenix.snackbar.FenixSnackbarDelegate
import org.mozilla.fenix.snackbar.SnackbarBinding
import org.mozilla.fenix.tor.TorSecurityLevel
import org.mozilla.fenix.tor.QuickstartViewModel
import org.mozilla.fenix.utils.Settings
import kotlin.system.exitProcess
import org.mozilla.fenix.GleanMetrics.Settings as SettingsMetrics
@@ -86,6 +90,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
    }
    private val snackbarBinding = ViewBoundFeatureWrapper<SnackbarBinding>()

    private val quickstartViewModel: QuickstartViewModel by activityViewModels()

    @VisibleForTesting
    internal val accountObserver = object : AccountObserver {
        private fun updateAccountUi(profile: Profile? = null) {
@@ -179,8 +185,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
    }

    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        runBlocking(context = Dispatchers.IO) {
            setPreferencesFromResource(R.xml.preferences, rootKey)
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
@@ -229,7 +237,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
        )

        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)
@@ -294,9 +306,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 = settings.getOpenLinksInAppsString()
        // val openLinksInAppsSettingsPreference =
        //     requirePreference<Preference>(R.string.pref_key_open_links_in_apps)
        // openLinksInAppsSettingsPreference.summary = settings.getOpenLinksInAppsString()

        setupPreferences(settings)

@@ -477,9 +489,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
                SettingsFragmentDirections.actionSettingsFragmentToLinkSharingFragment()
            }

            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_downloads) -> {
                SettingsFragmentDirections.actionSettingsFragmentToOpenDownloadsSettingsFragment()
@@ -618,6 +630,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
        setupHomepagePreference(settings)
        setupTrackingProtectionPreference(settings)
        setupDnsOverHttpsPreference(settings)
        setupConnectionPreferences()
    }

    /**
@@ -652,6 +665,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
        }
    }

    private fun RecyclerView.disableHidingAnimation() {
        this.setItemAnimator(null)
        this.setLayoutAnimation(null)
    }

    @VisibleForTesting
    internal fun setupAmoCollectionOverridePreference(
        settings: Settings,
@@ -758,6 +776,56 @@ 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<SwitchPreference>(R.string.pref_key_quick_start).apply {
            isChecked = quickstartViewModel.quickstart().value == true
            setOnPreferenceClickListener {
                quickstartViewModel.quickstartSet(
                    isChecked,
                )
                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
            }
        }
        requirePreference<Preference>(R.string.pref_key_about_config_shortcut).apply {
            isVisible = requireContext().settings().showSecretDebugMenuThisSession || Config.channel == ReleaseChannel.Debug
            setOnPreferenceClickListener {
                (requireActivity() as HomeActivity).openToBrowserAndLoad(
                    searchTermOrURL = "about:config",
                    from = BrowserDirection.FromSettings,
                    newTab = true,
                )
                true
            }
        }
    }

    @VisibleForTesting
    internal fun setupCookieBannerPreference(settings: Settings) {
        FxNimbus.features.cookieBanners.recordExposure()
+152 −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)
                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)
                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)
                }
                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)
            )
        }
    }
}
+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.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.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.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.compose.LocalLifecycleOwner
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),
                )
            },
            containerColor = PhotonColors.Violet50, // Same color as connect button
            contentColor = PhotonColors.LightGrey05,
        )
    }
}
+87 −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 mozilla.components.browser.engine.gecko.GeckoEngine
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.geckoview.TorAndroidIntegration.TorLogListener

class TorLogsViewModel(application: Application) : AndroidViewModel(application), TorLogListener {
    private val torController = application.components.torController
    private val engine = application.components.core.engine as GeckoEngine
    private val torAndroidIntegration = engine.getTorIntegrationController()
    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()
        torAndroidIntegration.registerLogListener(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()
        torAndroidIntegration.unregisterLogListener(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
    }
}
+14 −2
Original line number Diff line number Diff line
@@ -1038,6 +1038,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
    /**
     * Get the display string for the current open links in apps setting
     */
    /*
    fun getOpenLinksInAppsString(): String =
        when (openLinksInExternalApp) {
            appContext.getString(R.string.pref_key_open_links_in_apps_always) -> {
@@ -1054,6 +1055,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
                appContext.getString(R.string.preferences_open_links_in_apps_never)
            }
        }
    */

    var shouldUseDarkTheme by booleanPreference(
        appContext.getPreferenceKey(R.string.pref_key_dark_theme),
@@ -1809,26 +1811,31 @@ class Settings(private val appContext: Context) : PreferencesHolder {
    /**
     * Check to see if we should open the link in an external app
     */
    @Suppress("UNUSED_PARAMETER")
    fun shouldOpenLinksInApp(isCustomTab: Boolean = false): Boolean {
        return when (openLinksInExternalApp) {
        return false
        /*return when (openLinksInExternalApp) {
            appContext.getString(R.string.pref_key_open_links_in_apps_always) -> true
            appContext.getString(R.string.pref_key_open_links_in_apps_ask) -> true
            // Some applications will not work if custom tab never open links in apps, return true if it's custom tab
            appContext.getString(R.string.pref_key_open_links_in_apps_never) -> isCustomTab
            else -> false
        }
        }*/
    }

    /**
     * Check to see if we need to prompt the user if the link can be opened in an external app
     */
    fun shouldPromptOpenLinksInApp(): Boolean {
        return true
        /*
        return when (openLinksInExternalApp) {
            appContext.getString(R.string.pref_key_open_links_in_apps_always) -> false
            appContext.getString(R.string.pref_key_open_links_in_apps_ask) -> true
            appContext.getString(R.string.pref_key_open_links_in_apps_never) -> true
            else -> true
        }
        */
    }

    var openLinksInExternalApp by stringPreference(
@@ -2851,4 +2858,9 @@ class Settings(private val appContext: Context) : PreferencesHolder {
        val cleanupPreferenceKey = appContext.getString(R.string.pref_key_downloads_clean_up_files_automatically)
        return sharedPreferences.getBoolean(cleanupPreferenceKey, false)
    }

    var useHtmlConnectionUi by booleanPreference(
        key = appContext.getPreferenceKey(R.string.pref_key_use_html_connection_ui),
        default = false,
    )
}
Loading