For #16952 - Https-only mode support.

Default will be disabled with users having the possibility to enable this from
a new setting in the `Privacy and security` section.
If enabled then by default this force https for all tabs with the option for
users to switch to forcing https only on private tabs.
upstream-sync
Mugurell 2 years ago committed by mergify[bot]
parent 541b2be184
commit 46d757ab54

@ -26,6 +26,7 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromTrackingProtectionExceptions(R.id.trackingProtectionExceptionsFragment),
FromAbout(R.id.aboutFragment),
FromTrackingProtection(R.id.trackingProtectionFragment),
FromHttpsOnlyMode(R.id.httpsOnlyFragment),
FromTrackingProtectionDialog(R.id.trackingProtectionPanelDialogFragment),
FromSavedLoginsFragment(R.id.savedLoginsFragment),
FromAddNewDeviceFragment(R.id.addNewDeviceFragment),

@ -109,6 +109,7 @@ import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.perf.StartupTypeTelemetry
import org.mozilla.fenix.search.SearchDialogFragmentDirections
import org.mozilla.fenix.session.PrivateNotificationService
import org.mozilla.fenix.settings.HttpsOnlyFragmentDirections
import org.mozilla.fenix.settings.SettingsFragmentDirections
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
import org.mozilla.fenix.settings.about.AboutFragmentDirections
@ -798,6 +799,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
HistoryMetadataGroupFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtectionExceptions ->
TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHttpsOnlyMode ->
HttpsOnlyFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAbout ->
AboutFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtection ->

@ -122,7 +122,8 @@ class Core(
clearColor = ContextCompat.getColor(
context,
R.color.fx_mobile_layer_color_1
)
),
httpsOnlyMode = context.settings().getHttpsOnlyMode()
)
GeckoEngine(

@ -0,0 +1,119 @@
/* 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 android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.text.style.URLSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.text.HtmlCompat
import androidx.core.text.getSpans
import androidx.core.view.children
import androidx.fragment.app.Fragment
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.SettingsHttpsOnlyBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
// To be replaced with a SUMO link when available. This is the desktop link.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1758066.
private const val SUMO_URL = "https://support.mozilla.org/en-US/kb/https-only-prefs"
/**
* Lets the user customize HTTPS-only mode.
*/
class HttpsOnlyFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = SettingsHttpsOnlyBinding.inflate(inflater)
val summary = requireContext().getString(R.string.preferences_https_only_summary)
val learnMore = requireContext().getString(R.string.preferences_http_only_learn_more)
binding.httpsOnlySummary.run {
text = combineTextWithLink(summary, learnMore).apply {
setActionToUrlClick(this)
}
movementMethod = LinkMovementMethod.getInstance()
}
binding.httpsOnlySwitch.run {
isChecked = context.settings().shouldUseHttpsOnly
setHttpsModes(binding, isChecked)
setOnCheckedChangeListener { _, isHttpsOnlyEnabled ->
context.settings().shouldUseHttpsOnly = isHttpsOnlyEnabled
setHttpsModes(binding, isHttpsOnlyEnabled)
updateEngineHttpsOnlyMode()
}
}
// Since the http-only modes are in a RadioGroup we only need one listener to know of all their changes.
binding.httpsOnlyAllTabs.setOnCheckedChangeListener { _, _ ->
updateEngineHttpsOnlyMode()
}
return binding.root
}
private fun setHttpsModes(binding: SettingsHttpsOnlyBinding, isHttpsOnlyEnabled: Boolean) {
if (!isHttpsOnlyEnabled) {
binding.httpsOnlyModes.apply {
clearCheck()
children.forEach { it.isEnabled = false }
}
} else {
binding.httpsOnlyModes.children.forEach { it.isEnabled = true }
}
}
private fun updateEngineHttpsOnlyMode() {
requireContext().components.core.engine.settings.httpsOnlyMode =
requireContext().settings().getHttpsOnlyMode()
}
private fun combineTextWithLink(
text: String,
linkTitle: String
): SpannableStringBuilder {
val rawTextWithLink = HtmlCompat.fromHtml(
"$text <a href=\"\">$linkTitle</a>",
HtmlCompat.FROM_HTML_MODE_COMPACT
)
return SpannableStringBuilder(rawTextWithLink)
}
private fun setActionToUrlClick(
spannableStringBuilder: SpannableStringBuilder,
) {
val link = spannableStringBuilder.getSpans<URLSpan>()[0]
val linkStart = spannableStringBuilder.getSpanStart(link)
val linkEnd = spannableStringBuilder.getSpanEnd(link)
val linkFlags = spannableStringBuilder.getSpanFlags(link)
val linkClickListener: ClickableSpan = object : ClickableSpan() {
override fun onClick(view: View) {
view.setOnClickListener {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = SUMO_URL,
newTab = true,
from = BrowserDirection.FromHttpsOnlyMode
)
}
}
}
spannableStringBuilder.setSpan(linkClickListener, linkStart, linkEnd, linkFlags)
spannableStringBuilder.removeSpan(link)
}
}

@ -0,0 +1,73 @@
/* 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.content.Context
import android.util.AttributeSet
import android.widget.CompoundButton.OnCheckedChangeListener
import androidx.annotation.VisibleForTesting
import androidx.appcompat.widget.AppCompatRadioButton
import androidx.core.content.withStyledAttributes
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
/**
* [AppCompatRadioButton] backed by a boolean `SharedPreference`.
*
* Whenever this button is initialized it will attempt to set itself as checked with the
* current value of `R.styleable.PreferenceBackedRadioButton_preferenceKey` defaulting to
* `R.styleable.PreferenceBackedRadioButton_preferenceKeyDefaultValue` if there is no value set
* for the indicated `SharedPreference`.
*
* Whenever the radio button is enabled or disabled this will be persisted in a `SharedPreference`
* with the name indicated in `R.styleable.PreferenceBackedRadioButton_preferenceKey` .
*/
class PreferenceBackedRadioButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.radioButtonStyle,
) : AppCompatRadioButton(context, attrs, defStyleAttr) {
@VisibleForTesting
internal var externalOnCheckedChangeListener: OnCheckedChangeListener? = null
@VisibleForTesting
internal var backingPreferenceName: String? = null
@VisibleForTesting
internal var backingPreferenceDefaultValue: Boolean = false
private val internalOnCheckedChangeListener = OnCheckedChangeListener { buttonView, isChecked ->
backingPreferenceName?.let {
context.settings().preferences.edit().putBoolean(it, isChecked).apply()
}
externalOnCheckedChangeListener?.onCheckedChanged(buttonView, isChecked)
}
init {
context.withStyledAttributes(attrs, R.styleable.PreferenceBackedRadioButton, defStyleAttr, 0) {
backingPreferenceName = this.getString(R.styleable.PreferenceBackedRadioButton_preferenceKey)
backingPreferenceDefaultValue = getBoolean(
R.styleable.PreferenceBackedRadioButton_preferenceKeyDefaultValue, false
)
}
isChecked = context.settings().preferences.getBoolean(backingPreferenceName, backingPreferenceDefaultValue)
super.setOnCheckedChangeListener(internalOnCheckedChangeListener)
}
override fun setOnCheckedChangeListener(listener: OnCheckedChangeListener?) {
externalOnCheckedChangeListener = listener
}
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
if (enabled) {
isChecked = context.settings().preferences.getBoolean(backingPreferenceName, backingPreferenceDefaultValue)
} else {
context.settings().preferences.edit().remove(backingPreferenceName).apply()
}
}
}

@ -259,6 +259,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
resources.getString(R.string.pref_key_private_browsing) -> {
SettingsFragmentDirections.actionSettingsFragmentToPrivateBrowsingFragment()
}
resources.getString(R.string.pref_key_https_only_settings) -> {
SettingsFragmentDirections.actionSettingsFragmentToHttpsOnlyFragment()
}
resources.getString(R.string.pref_key_accessibility) -> {
SettingsFragmentDirections.actionSettingsFragmentToAccessibilityFragment()
}
@ -467,6 +470,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
setupAmoCollectionOverridePreference(requireContext().settings())
setupAllowDomesticChinaFxaServerPreference()
setupHttpsOnlyPreferences()
}
/**
@ -604,6 +608,19 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}
@VisibleForTesting
internal fun setupHttpsOnlyPreferences() {
val httpsOnlyPreference =
requirePreference<Preference>(R.string.pref_key_https_only_settings)
httpsOnlyPreference.summary = context?.let {
if (it.settings().shouldUseHttpsOnly) {
getString(R.string.preferences_https_only_on)
} else {
getString(R.string.preferences_https_only_off)
}
}
}
private fun isDefaultBrowserExperimentBranch(): Boolean =
requireContext().settings().isDefaultBrowserMessageLocation(MessageSurfaceId.SETTINGS)

@ -15,6 +15,7 @@ import android.view.accessibility.AccessibilityManager
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE
import androidx.lifecycle.LifecycleOwner
import mozilla.components.concept.engine.Engine.HttpsOnlyMode
import mozilla.components.feature.sitepermissions.SitePermissionsRules
import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action
import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction
@ -508,6 +509,21 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = false
)
var shouldUseHttpsOnly by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_https_only),
default = false
)
var shouldUseHttpsOnlyInAllTabs by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_https_only_in_all_tabs),
default = true
)
var shouldUseHttpsOnlyInPrivateTabsOnly by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_https_only_in_private_tabs),
default = false
)
var shouldUseTrackingProtection by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_tracking_protection),
default = true
@ -1287,4 +1303,17 @@ class Settings(private val appContext: Context) : PreferencesHolder {
appContext.getPreferenceKey(R.string.pref_key_home_blocklist),
default = setOf()
)
/**
* Get the current mode for how https-only is enabled.
*/
fun getHttpsOnlyMode(): HttpsOnlyMode {
return if (!shouldUseHttpsOnly) {
HttpsOnlyMode.DISABLED
} else if (shouldUseHttpsOnlyInPrivateTabsOnly) {
HttpsOnlyMode.ENABLED_PRIVATE_ONLY
} else {
HttpsOnlyMode.ENABLED
}
}
}

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/https_only_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="2dp"
android:clickable="false"
android:lineHeight="24.sp"
android:text="@string/preferences_https_only_title"
android:textAppearance="@style/ListItemTextStyle"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/https_only_summary"
app:layout_constraintEnd_toStartOf="@id/https_only_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:targetApi="p" />
<TextView
android:id="@+id/https_only_summary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:lineHeight="16.sp"
android:textColor="?attr/textSecondary"
android:textColorLink="?attr/textSecondary"
android:textSize="12sp"
app:layout_constraintBottom_toTopOf="@id/https_only_modes"
app:layout_constraintEnd_toEndOf="@id/https_only_title"
app:layout_constraintStart_toStartOf="@id/https_only_title"
app:layout_constraintTop_toBottomOf="@id/https_only_title"
tools:targetApi="p"
tools:text="@string/preferences_https_only_summary" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/https_only_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:paddingStart="18dp"
android:paddingEnd="18dp"
android:textColor="@color/state_list_text_color"
android:textOff="@string/studies_off"
android:textOn="@string/studies_on"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/https_only_title" />
<RadioGroup
android:id="@+id/https_only_modes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/https_only_summary">
<org.mozilla.fenix.settings.PreferenceBackedRadioButton
android:id="@+id/https_only_all_tabs"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackground"
android:button="@null"
android:drawablePadding="@dimen/radio_button_preference_drawable_padding"
android:paddingStart="@dimen/radio_button_preference_horizontal"
android:paddingTop="@dimen/radio_button_preference_vertical"
android:paddingEnd="@dimen/radio_button_preference_horizontal"
android:paddingBottom="@dimen/radio_button_preference_vertical"
android:text="@string/preferences_https_only_in_all_tabs"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp"
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle"
app:preferenceKey="@string/pref_key_https_only_in_all_tabs"
app:preferenceKeyDefaultValue="true" />
<org.mozilla.fenix.settings.PreferenceBackedRadioButton
android:id="@+id/https_only_private_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:button="@null"
android:drawablePadding="@dimen/radio_button_preference_drawable_padding"
android:paddingStart="@dimen/radio_button_preference_horizontal"
android:paddingTop="@dimen/radio_button_preference_vertical"
android:paddingEnd="@dimen/radio_button_preference_horizontal"
android:paddingBottom="@dimen/radio_button_preference_vertical"
android:text="@string/preferences_https_only_in_private_tabs"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp"
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle"
app:preferenceKey="@string/pref_key_https_only_in_private_tabs"
app:preferenceKeyDefaultValue="false" />
</RadioGroup>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -586,6 +586,13 @@
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_httpsOnlyFragment"
app:destination="@id/httpsOnlyFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_trackingProtectionFragment"
app:destination="@id/trackingProtectionFragment"
@ -767,6 +774,10 @@
android:id="@+id/privateBrowsingFragment"
android:name="org.mozilla.fenix.settings.PrivateBrowsingFragment"
android:label="@string/preferences_private_browsing_options" />
<fragment
android:id="@+id/httpsOnlyFragment"
android:name="org.mozilla.fenix.settings.HttpsOnlyFragment"
android:label="@string/preferences_https_only_title" />
<fragment
android:id="@+id/trackingProtectionFragment"
android:name="org.mozilla.fenix.settings.TrackingProtectionFragment">

@ -120,4 +120,9 @@
<attr format="string|boolean|integer|reference|float" name="defaultValue"/>
<attr name="android:defaultValue"/>
</declare-styleable>
<declare-styleable name="PreferenceBackedRadioButton">
<attr format="string" name="preferenceKey"/>
<attr format="boolean" name="preferenceKeyDefaultValue"/>
</declare-styleable>
</resources>

@ -141,6 +141,12 @@
<string name="pref_key_recent_tabs" translatable="false">pref_key_recent_tabs</string>
<string name="pref_key_recent_bookmarks" translatable="false">pref_key_recent_bookmarks</string>
<!-- HTTPS Only Settings -->
<string name="pref_key_https_only_settings" translatable="false">pref_key_https_only_settings</string>
<string name="pref_key_https_only" translatable="false">pref_key_https_only</string>
<string name="pref_key_https_only_in_all_tabs" translatable="false">pref_key_https_only_in_all_tabs</string>
<string name="pref_key_https_only_in_private_tabs" translatable="false">pref_key_https_only_in_private_tabs</string>
<!-- Tracking Protection Settings -->
<string name="pref_key_etp_learn_more" translatable="false">pref_key_etp_learn_more</string>
<string name="pref_key_tracking_protection_settings" translatable="false">pref_key_tracking_protection_settings</string>

@ -316,6 +316,20 @@
<string name="preferences_screenshots_in_private_mode_disclaimer">If allowed, private tabs will also be visible when multiple apps are open</string>
<!-- Preference for adding private browsing shortcut -->
<string name="preferences_add_private_browsing_shortcut">Add private browsing shortcut</string>
<!-- Preference for enabling "HTTPS-Only" mode -->
<string name="preferences_https_only_title">HTTPS-Only Mode</string>
<!-- Description of the preference to enable "HTTPS-Only" mode. -->
<string name="preferences_https_only_summary">Automatically attempts to connect to sites using HTTPS encryption protocol for increased security.</string>
<!-- Summary of tracking protection preference if tracking protection is set to on -->
<string name="preferences_https_only_on">On</string>
<!-- Summary of tracking protection preference if tracking protection is set to off -->
<string name="preferences_https_only_off">Off</string>
<!-- Text displayed that links to website containing documentation about "HTTPS-Only" mode -->
<string name="preferences_http_only_learn_more">Learn more</string>
<!-- Option for the https only setting -->
<string name="preferences_https_only_in_all_tabs">Enable in all tabs</string>
<!-- Option for the https only setting -->
<string name="preferences_https_only_in_private_tabs">Enable only in private tabs</string>
<!-- Preference for accessibility -->
<string name="preferences_accessibility">Accessibility</string>
<!-- Preference to override the Firefox Account server -->

@ -105,6 +105,11 @@
app:iconSpaceReserved="false"
android:title="@string/preferences_private_browsing_options" />
<androidx.preference.Preference
android:key="@string/pref_key_https_only_settings"
app:iconSpaceReserved="false"
android:title="@string/preferences_https_only_title" />
<androidx.preference.Preference
android:key="@string/pref_key_tracking_protection_settings"
app:iconSpaceReserved="false"

@ -11,6 +11,7 @@ import android.view.View
import android.widget.FrameLayout
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
@ -19,6 +20,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import java.io.File
@ -56,17 +58,23 @@ class PerformanceInflaterTest {
@Test
fun `WHEN inflating one of our resource file, the inflater should not crash`() {
val fileList = File("./src/main/res/layout").listFiles()
for (file in fileList!!) {
val layoutName = file.name.split(".")[0]
val layoutId = testContext.resources.getIdentifier(
layoutName,
"layout",
testContext.packageName
)
assertNotEquals(-1, layoutId)
if (!layoutsNotToTest.contains(layoutName)) {
perfInflater.inflate(layoutId, FrameLayout(testContext), true)
// There might be custom views who try to access `Settings` through the extension function.
mockkStatic("org.mozilla.fenix.ext.ContextKt") {
every { any<Context>().settings() } returns mockk(relaxed = true)
for (file in fileList!!) {
val layoutName = file.name.split(".")[0]
val layoutId = testContext.resources.getIdentifier(
layoutName,
"layout",
testContext.packageName
)
assertNotEquals(-1, layoutId)
if (!layoutsNotToTest.contains(layoutName)) {
perfInflater.inflate(layoutId, FrameLayout(testContext), true)
}
}
}
}

@ -0,0 +1,147 @@
/* 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.content.Context
import android.content.SharedPreferences.Editor
import android.widget.CompoundButton.OnCheckedChangeListener
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.spyk
import io.mockk.verify
import io.mockk.verifyOrder
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.utils.Settings
import org.robolectric.Robolectric
import kotlin.random.Random
@RunWith(FenixRobolectricTestRunner::class)
class PreferenceBackedRadioButtonTest {
@Test
fun `GIVEN a preference key is provided WHEN initialized THEN cache the value`() {
val attributes = Robolectric.buildAttributeSet()
.addAttribute(R.attr.preferenceKey, "test")
.build()
every { testContext.settings().preferences.getBoolean(any(), any()) } returns Random.nextBoolean()
val button = PreferenceBackedRadioButton(testContext, attributes)
assertEquals("test", button.backingPreferenceName)
}
@Test
fun `GIVEN a default preference value is provided WHEN initialized THEN cache the value`() {
val attributes = Robolectric.buildAttributeSet()
.addAttribute(R.attr.preferenceKeyDefaultValue, "true")
.build()
every { testContext.settings().preferences.getBoolean(any(), any()) } returns Random.nextBoolean()
val button = PreferenceBackedRadioButton(testContext, attributes)
assertTrue(button.backingPreferenceDefaultValue)
}
@Test
fun `GIVEN a default preference value is not provided WHEN initialized THEN remember the default value as false`() {
val attributes = Robolectric.buildAttributeSet().build()
every { testContext.settings().preferences.getBoolean(any(), any()) } returns Random.nextBoolean()
val button = PreferenceBackedRadioButton(testContext, attributes)
assertFalse(button.backingPreferenceDefaultValue)
}
@Test
fun `GIVEN the backing preference doesn't have a value set WHEN initialized THEN set if checked the default value`() {
val attributes = Robolectric.buildAttributeSet()
.addAttribute(R.attr.preferenceKeyDefaultValue, "true")
.build()
mockkStatic("org.mozilla.fenix.ext.ContextKt") {
every { any<Context>().settings() } returns Settings(testContext)
val button = PreferenceBackedRadioButton(testContext, attributes)
assertTrue(button.isChecked)
}
}
@Test
fun `GIVEN there is no backing preference or default value set vaWHEN initialized THEN set if checked as false`() {
val attributes = Robolectric.buildAttributeSet().build()
mockkStatic("org.mozilla.fenix.ext.ContextKt") {
every { any<Context>().settings() } returns Settings(testContext)
val button = PreferenceBackedRadioButton(testContext, attributes)
assertFalse(button.isChecked)
}
}
@Test
fun `GIVEN the backing preference does have a value set WHEN initialized THEN set if checked the value from the preference`() {
val attributes = Robolectric.buildAttributeSet()
.addAttribute(R.attr.preferenceKey, "test")
.build()
every { testContext.settings().preferences.getBoolean(eq("test"), any()) } returns true
val button = PreferenceBackedRadioButton(testContext, attributes)
assertTrue(button.isChecked)
}
@Test
fun `WHEN a OnCheckedChangeListener is set THEN cache it internally`() {
every { testContext.settings().preferences.getBoolean(any(), any()) } returns Random.nextBoolean()
val button = PreferenceBackedRadioButton(testContext)
val testListener: OnCheckedChangeListener = mockk()
button.setOnCheckedChangeListener(testListener)
assertSame(testListener, button.externalOnCheckedChangeListener)
}
@Test
fun `GIVEN a OnCheckedChangeListener is set WHEN the checked status changes THEN update the backing preference and then inform the listener`() {
val editor: Editor = mockk(relaxed = true)
every { testContext.settings().preferences.edit() } returns editor
// set the button initially as not checked
every { testContext.settings().preferences.getBoolean(any(), any()) } returns false
val button = PreferenceBackedRadioButton(testContext)
button.backingPreferenceName = "test"
val testListener: OnCheckedChangeListener = mockk(relaxed = true)
button.externalOnCheckedChangeListener = testListener
button.isChecked = true
verifyOrder {
editor.putBoolean("test", true)
testListener.onCheckedChanged(any(), any())
}
}
@Test
fun `WHEN the button gets enabled THEN set isChecked based on the value from the backing preference`() {
every { testContext.settings().preferences.getBoolean(any(), any()) } returns true
val button = spyk(PreferenceBackedRadioButton(testContext))
button.isEnabled = true
verify(exactly = 1) { // first "isChecked" from init happens before we can count it
button.isChecked = true
}
}
}

@ -13,6 +13,7 @@ import mozilla.components.concept.fetch.Client
import mozilla.components.service.nimbus.NimbusDisabled
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
@ -25,6 +26,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.ReleaseChannel
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.utils.Settings
@ -37,6 +39,7 @@ class SettingsFragmentTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
private val testDispatcher = coroutinesTestRule.testDispatcher
private val settingsFragment = SettingsFragment()
@Before
fun setup() {
@ -53,6 +56,11 @@ class SettingsFragmentTest {
every { Config.channel } returns ReleaseChannel.Nightly
FxNimbus.api = NimbusDisabled(testContext)
val activity = Robolectric.buildActivity(FragmentActivity::class.java).create().get()
activity.supportFragmentManager.beginTransaction()
.add(settingsFragment, "test")
.commitNow()
}
@Test
@ -110,4 +118,32 @@ class SettingsFragmentTest {
settingsFragment.setupAmoCollectionOverridePreference(settings)
assertTrue(preferenceAmoCollectionOverride.isVisible)
}
@Test
fun `GIVEN the HttpsOnly is enabled THEN set the appropriate preference summary`() {
val httpsOnlyPreference = settingsFragment.findPreference<Preference>(
settingsFragment.getPreferenceKey(R.string.pref_key_https_only_settings)
)!!
every { testContext.settings().shouldUseHttpsOnly } returns true
assertTrue(httpsOnlyPreference.summary.isNullOrEmpty())
val summary = testContext.getString(R.string.preferences_https_only_on)
settingsFragment.setupHttpsOnlyPreferences()
assertEquals(summary, httpsOnlyPreference.summary)
}
@Test
fun `GIVEN the HttpsOnly is disabled THEN set the appropriate preference summary`() {
val httpsOnlyPreference = settingsFragment.findPreference<Preference>(
settingsFragment.getPreferenceKey(R.string.pref_key_https_only_settings)
)!!
every { testContext.settings().shouldUseHttpsOnly } returns false
assertTrue(httpsOnlyPreference.summary.isNullOrEmpty())
val summary = testContext.getString(R.string.preferences_https_only_off)
settingsFragment.setupHttpsOnlyPreferences()
assertEquals(summary, httpsOnlyPreference.summary)
}
}

@ -6,6 +6,9 @@ package org.mozilla.fenix.utils
import io.mockk.every
import io.mockk.spyk
import mozilla.components.concept.engine.Engine.HttpsOnlyMode.DISABLED
import mozilla.components.concept.engine.Engine.HttpsOnlyMode.ENABLED
import mozilla.components.concept.engine.Engine.HttpsOnlyMode.ENABLED_PRIVATE_ONLY
import mozilla.components.feature.sitepermissions.SitePermissionsRules
import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.ALLOWED
import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.ASK_TO_ALLOW
@ -800,4 +803,61 @@ class SettingsTest {
assertTrue(settings.shouldShowInactiveTabsAutoCloseDialog(20))
}
@Test
fun `GIVEN Https-only mode is disabled THEN the engine mode is HttpsOnlyMode#DISABLED`() {
settings.shouldUseHttpsOnly = false
val result = settings.getHttpsOnlyMode()
assertEquals(DISABLED, result)
}
@Test
fun `GIVEN Https-only mode is enabled THEN the engine mode is HttpsOnlyMode#ENABLED`() {
settings.shouldUseHttpsOnly = true
val result = settings.getHttpsOnlyMode()
assertEquals(ENABLED, result)
}
@Test
fun `GIVEN Https-only mode is enabled for all tabs THEN the engine mode is HttpsOnlyMode#ENABLED`() {
settings.apply {
shouldUseHttpsOnly = true
shouldUseHttpsOnlyInAllTabs = true
}
val result = settings.getHttpsOnlyMode()
assertEquals(ENABLED, result)
}
@Test
fun `GIVEN Https-only mode is enabled for only private tabs THEN the engine mode is HttpsOnlyMode#ENABLED_PRIVATE_ONLY`() {
settings.apply {
shouldUseHttpsOnly = true
shouldUseHttpsOnlyInPrivateTabsOnly = true
}
val result = settings.getHttpsOnlyMode()
assertEquals(ENABLED_PRIVATE_ONLY, result)
}
@Test
fun `GIVEN unset user preferences THEN https-only is disabled`() {
assertFalse(settings.shouldUseHttpsOnly)
}
@Test
fun `GIVEN unset user preferences THEN https-only is enabled for all tabs`() {
assertTrue(settings.shouldUseHttpsOnlyInAllTabs)
}
@Test
fun `GIVEN unset user preferences THEN https-only is disabled for private tabs`() {
assertFalse(settings.shouldUseHttpsOnlyInPrivateTabsOnly)
}
}

Loading…
Cancel
Save