Merge remote-tracking branch 'origin/fenix/122.0' into iceraven
commit
279f9e954c
@ -1 +1 @@
|
||||
Subproject commit 81883d4e4f0f81a00b67875a5f592480b898f3ce
|
||||
Subproject commit a2934ef2d89153e3f7842dcfe71fa81af9888d68
|
@ -0,0 +1,52 @@
|
||||
# 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/.
|
||||
---
|
||||
# This file configures "evergreen" messages that are displayed via
|
||||
# the Nimbus Messaging system.
|
||||
#
|
||||
# They are "evergreen" in that they apply to all users, and shipped with the app.
|
||||
#
|
||||
# This file is intended to grow new messages once messages have been tested via
|
||||
# experiment, rolled out to everyone in the release, and are ready to be rolled out
|
||||
# without the remote prompting from Experimenter.
|
||||
#
|
||||
# When adding new messages to this file, please add the experiment (and/or rollout) URLs used to
|
||||
# validate them.
|
||||
#
|
||||
# Triggers, actions and styles are configured in messaging-fenix.fml.yaml.
|
||||
import:
|
||||
- path: ../android-components/components/service/nimbus/messaging.fml.yaml
|
||||
channel: release
|
||||
features:
|
||||
messaging:
|
||||
# This message displays on the homescreen, asking the user to set Firefox as the default.
|
||||
# It is triggered after a minimum of 4 launches of the app.
|
||||
- value:
|
||||
messages:
|
||||
default-browser:
|
||||
text: default_browser_experiment_card_text
|
||||
surface: homescreen
|
||||
action: "MAKE_DEFAULT_BROWSER"
|
||||
trigger:
|
||||
- I_AM_NOT_DEFAULT_BROWSER
|
||||
- USER_ESTABLISHED_INSTALL
|
||||
style: PERSISTENT
|
||||
button-label: preferences_set_as_default_browser
|
||||
|
||||
triggers:
|
||||
USER_ESTABLISHED_INSTALL: "number_of_app_launches >=4"
|
||||
|
||||
# This message displays as a 'push' notification, asking the user to set Firefox as the default.
|
||||
# It is triggered three days after install.
|
||||
- value:
|
||||
messages:
|
||||
default-browser-notification:
|
||||
title: nimbus_notification_default_browser_title
|
||||
text: nimbus_notification_default_browser_text
|
||||
surface: notification
|
||||
style: NOTIFICATION
|
||||
trigger:
|
||||
- I_AM_NOT_DEFAULT_BROWSER
|
||||
- DAY_3_AFTER_INSTALL
|
||||
action: MAKE_DEFAULT_BROWSER
|
@ -0,0 +1,112 @@
|
||||
# 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/.
|
||||
---
|
||||
includes:
|
||||
- messaging-evergreen-messages.fml.yaml
|
||||
import:
|
||||
- path: ../android-components/components/service/nimbus/messaging.fml.yaml
|
||||
channel: release
|
||||
features:
|
||||
messaging:
|
||||
- value:
|
||||
triggers:
|
||||
# Using attributes built into the Nimbus SDK
|
||||
USER_RECENTLY_INSTALLED: days_since_install < 7
|
||||
USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update
|
||||
USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale)
|
||||
USER_EN_SPEAKER: "'en' in locale"
|
||||
USER_ES_SPEAKER: "'es' in locale"
|
||||
USER_DE_SPEAKER: "'de' in locale"
|
||||
USER_FR_SPEAKER: "'fr' in locale"
|
||||
DEVICE_ANDROID: os == 'Android'
|
||||
DEVICE_IOS: os == 'iOS'
|
||||
ALWAYS: "true"
|
||||
NEVER: "false"
|
||||
DAY_1_AFTER_INSTALL: days_since_install == 1
|
||||
DAY_2_AFTER_INSTALL: days_since_install == 2
|
||||
DAY_3_AFTER_INSTALL: days_since_install == 3
|
||||
DAY_4_AFTER_INSTALL: days_since_install == 4
|
||||
DAY_5_AFTER_INSTALL: days_since_install == 5
|
||||
MORE_THAN_24H_SINCE_INSTALLED_OR_UPDATED: days_since_update >= 1
|
||||
|
||||
# Using custom attributes for the browser
|
||||
I_AM_DEFAULT_BROWSER: "is_default_browser"
|
||||
I_AM_NOT_DEFAULT_BROWSER: "is_default_browser == false"
|
||||
|
||||
FUNNEL_PAID: "adjust_campaign != ''"
|
||||
FUNNEL_ORGANIC: "adjust_campaign == ''"
|
||||
|
||||
# Using Glean events, specific to the browser
|
||||
INACTIVE_1_DAY: "'app_launched'|eventLastSeen('Hours') >= 24"
|
||||
INACTIVE_2_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 2"
|
||||
INACTIVE_3_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 3"
|
||||
INACTIVE_4_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 4"
|
||||
INACTIVE_5_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 5"
|
||||
|
||||
# Has the user signed in the last 4 years
|
||||
FXA_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) <= 4"
|
||||
FXA_NOT_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) > 4"
|
||||
|
||||
# https://mozilla-hub.atlassian.net/wiki/spaces/FJT/pages/11469471/Core+Active
|
||||
USER_INFREQUENT: "'app_launched'|eventCountNonZero('Days', 28) >= 1 && 'app_launched'|eventCountNonZero('Days', 28) < 7"
|
||||
USER_CASUAL: "'app_launched'|eventCountNonZero('Days', 28) >= 7 && 'app_launched'|eventCountNonZero('Days', 28) < 14"
|
||||
USER_REGULAR: "'app_launched'|eventCountNonZero('Days', 28) >= 14 && 'app_launched'|eventCountNonZero('Days', 28) < 21"
|
||||
USER_CORE_ACTIVE: "'app_launched'|eventCountNonZero('Days', 28) >= 21"
|
||||
|
||||
LAUNCHED_ONCE_THIS_WEEK: "'app_launched'|eventSum('Days', 7) == 1"
|
||||
|
||||
actions:
|
||||
ENABLE_PRIVATE_BROWSING: ://enable_private_browsing
|
||||
INSTALL_SEARCH_WIDGET: ://install_search_widget
|
||||
MAKE_DEFAULT_BROWSER: ://make_default_browser
|
||||
VIEW_BOOKMARKS: ://urls_bookmarks
|
||||
VIEW_COLLECTIONS: ://home_collections
|
||||
VIEW_HISTORY: ://urls_history
|
||||
VIEW_HOMESCREEN: ://home
|
||||
OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility
|
||||
OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager
|
||||
OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data
|
||||
OPEN_SETTINGS_LOGINS: ://settings_logins
|
||||
OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications
|
||||
OPEN_SETTINGS_PRIVACY: ://settings_privacy
|
||||
OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine
|
||||
OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection
|
||||
OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers
|
||||
OPEN_SETTINGS: ://settings
|
||||
TURN_ON_SYNC: ://turn_on_sync
|
||||
styles:
|
||||
DEFAULT:
|
||||
priority: 50
|
||||
max-display-count: 5
|
||||
SURVEY:
|
||||
priority: 55
|
||||
max-display-count: 1
|
||||
PERSISTENT:
|
||||
priority: 50
|
||||
max-display-count: 20
|
||||
WARNING:
|
||||
priority: 60
|
||||
max-display-count: 10
|
||||
URGENT:
|
||||
priority: 100
|
||||
max-display-count: 10
|
||||
NOTIFICATION:
|
||||
priority: 50
|
||||
max-display-count: 1
|
||||
$$surfaces:
|
||||
- homescreen
|
||||
- notification
|
||||
- survey
|
||||
|
||||
- channel: developer
|
||||
value:
|
||||
styles:
|
||||
DEFAULT:
|
||||
priority: 50
|
||||
max-display-count: 100
|
||||
EXPIRES_QUICKLY:
|
||||
priority: 100
|
||||
max-display-count: 1
|
||||
notification-config:
|
||||
refresh-interval: 120 # minutes (2 hours)
|
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<body>
|
||||
<script type = "text/javascript" >
|
||||
const gpcValue = navigator.globalPrivacyControl
|
||||
if (gpcValue) {
|
||||
document.write('<p>GPC is enabled.</p>');
|
||||
} else {
|
||||
document.write('<p>GPC not enabled.</p>');
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1,23 @@
|
||||
From: https://raw.githubusercontent.com/fonttools/fonttools/main/LICENSE
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Just van Rossum
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,79 @@
|
||||
package org.mozilla.fenix.components
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.components.metrics.fonts.FontEnumerationWorker
|
||||
import org.mozilla.fenix.components.metrics.fonts.FontParser
|
||||
|
||||
class FontParserTest {
|
||||
@Test
|
||||
fun testSanityAssertion() {
|
||||
/*
|
||||
Changing the below constant causes _all_ Nightly users to send a (large) Telemetry event containing
|
||||
their font information. Do not change this value unless you explicitly intend this.
|
||||
*/
|
||||
assertEquals(4, FontEnumerationWorker.kDesiredSubmissions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFontParsing() {
|
||||
val assetManager = InstrumentationRegistry.getInstrumentation().context.assets
|
||||
val font1 = FontParser.parse("no-path", assetManager.open("resources/TestTTF.ttf"))
|
||||
assertEquals(
|
||||
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T" +
|
||||
"\u0000T\u0000F",
|
||||
font1.family,
|
||||
)
|
||||
assertEquals(
|
||||
"\u0000V\u0000e\u0000r\u0000s\u0000i\u0000o\u0000n\u0000 \u00001\u0000." +
|
||||
"\u00000\u00000\u00000",
|
||||
font1.fontVersion,
|
||||
)
|
||||
assertEquals(
|
||||
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F",
|
||||
font1.fullName,
|
||||
)
|
||||
assertEquals("\u0000R\u0000e\u0000g\u0000u\u0000l\u0000a\u0000r", font1.subFamily)
|
||||
assertEquals(
|
||||
"\u0000F\u0000o\u0000n\u0000t\u0000T\u0000o\u0000o\u0000l\u0000s\u0000:\u0000 " +
|
||||
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F\u0000:\u0000 \u00002\u00000\u00001\u00005",
|
||||
font1.uniqueSubFamily,
|
||||
)
|
||||
assertEquals(
|
||||
"C4E8CE309F44A131D061D73B2580E922A7F5ECC8D7109797AC0FF58BF8723B7B",
|
||||
font1.hash,
|
||||
)
|
||||
assertEquals(3516272951, font1.created)
|
||||
assertEquals(3573411749, font1.modified)
|
||||
assertEquals(65536, font1.revision)
|
||||
val font2 = FontParser.parse("no-path", assetManager.open("resources/TestTTC.ttc"))
|
||||
assertEquals(
|
||||
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T" +
|
||||
"\u0000T\u0000F",
|
||||
font2.family,
|
||||
)
|
||||
assertEquals(
|
||||
"\u0000V\u0000e\u0000r\u0000s\u0000i\u0000o\u0000n\u0000 \u00001\u0000." +
|
||||
"\u00000\u00000\u00000",
|
||||
font2.fontVersion,
|
||||
)
|
||||
assertEquals(
|
||||
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F",
|
||||
font2.fullName,
|
||||
)
|
||||
assertEquals("\u0000R\u0000e\u0000g\u0000u\u0000l\u0000a\u0000r", font1.subFamily)
|
||||
assertEquals(
|
||||
"\u0000F\u0000o\u0000n\u0000t\u0000T\u0000o\u0000o\u0000l\u0000s\u0000:\u0000 " +
|
||||
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F\u0000:\u0000 \u00002\u00000\u00001\u00005",
|
||||
font2.uniqueSubFamily,
|
||||
)
|
||||
assertEquals(
|
||||
"A8521588045ED5F1F8B07EECAAC06ED3186C644655BFAC00DD4507CD316FBDC5",
|
||||
font2.hash,
|
||||
)
|
||||
assertEquals(3516272951, font2.created)
|
||||
assertEquals(3573411749, font2.modified)
|
||||
assertEquals(65536, font2.revision)
|
||||
}
|
||||
}
|
@ -1,17 +1,25 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
|
||||
def test_experiment_unenrolls_via_studies_toggle(setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment):
|
||||
def test_experiment_unenrolls_via_studies_toggle(
|
||||
setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
|
||||
):
|
||||
setup_experiment(load_branches)
|
||||
gradlewbuild.test("GenericExperimentIntegrationTest#disableStudiesViaStudiesToggle")
|
||||
assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
|
||||
gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolled")
|
||||
assert check_ping_for_experiment(reason="unenrollment", branch=load_branches[0])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
|
||||
def test_experiment_unenrolls_via_secret_menu(setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment):
|
||||
def test_experiment_unenrolls_via_secret_menu(
|
||||
setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
|
||||
):
|
||||
setup_experiment(load_branches)
|
||||
gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolledViaSecretMenu")
|
||||
gradlewbuild.test(
|
||||
"GenericExperimentIntegrationTest#testExperimentUnenrolledViaSecretMenu"
|
||||
)
|
||||
assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
|
||||
gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolled")
|
||||
assert check_ping_for_experiment(reason="unenrollment", branch=load_branches[0])
|
||||
|
@ -1,36 +1,36 @@
|
||||
/* 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.helpers
|
||||
|
||||
import android.view.View
|
||||
import androidx.test.espresso.IdlingResource
|
||||
|
||||
class ViewVisibilityIdlingResource(
|
||||
private val view: View,
|
||||
private val expectedVisibility: Int,
|
||||
) : IdlingResource {
|
||||
private var resourceCallback: IdlingResource.ResourceCallback? = null
|
||||
private var isIdle: Boolean = false
|
||||
|
||||
override fun getName(): String {
|
||||
return ViewVisibilityIdlingResource::class.java.name + ":" + view.id + ":" + expectedVisibility
|
||||
}
|
||||
|
||||
override fun isIdleNow(): Boolean {
|
||||
if (isIdle) return true
|
||||
|
||||
isIdle = view.visibility == expectedVisibility
|
||||
|
||||
if (isIdle) {
|
||||
resourceCallback?.onTransitionToIdle()
|
||||
}
|
||||
|
||||
return isIdle
|
||||
}
|
||||
|
||||
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
|
||||
this.resourceCallback = callback
|
||||
}
|
||||
}
|
||||
/* 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.helpers
|
||||
|
||||
import android.view.View
|
||||
import androidx.test.espresso.IdlingResource
|
||||
|
||||
class ViewVisibilityIdlingResource(
|
||||
private val view: View,
|
||||
private val expectedVisibility: Int,
|
||||
) : IdlingResource {
|
||||
private var resourceCallback: IdlingResource.ResourceCallback? = null
|
||||
private var isIdle: Boolean = false
|
||||
|
||||
override fun getName(): String {
|
||||
return ViewVisibilityIdlingResource::class.java.name + ":" + view.id + ":" + expectedVisibility
|
||||
}
|
||||
|
||||
override fun isIdleNow(): Boolean {
|
||||
if (isIdle) return true
|
||||
|
||||
isIdle = view.visibility == expectedVisibility
|
||||
|
||||
if (isIdle) {
|
||||
resourceCallback?.onTransitionToIdle()
|
||||
}
|
||||
|
||||
return isIdle
|
||||
}
|
||||
|
||||
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
|
||||
this.resourceCallback = callback
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,194 @@
|
||||
/* 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.ui
|
||||
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.customannotations.SmokeTest
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.helpers.AppAndSystemHelper
|
||||
import org.mozilla.fenix.helpers.HomeActivityTestRule
|
||||
import org.mozilla.fenix.helpers.TestHelper
|
||||
import org.mozilla.fenix.ui.robots.navigationToolbar
|
||||
|
||||
/**
|
||||
* Tests for verifying the Firefox suggest search fragment
|
||||
*
|
||||
*/
|
||||
|
||||
class FirefoxSuggestTest {
|
||||
@get:Rule
|
||||
val activityTestRule = AndroidComposeTestRule(
|
||||
HomeActivityTestRule(
|
||||
skipOnboarding = true,
|
||||
isPocketEnabled = false,
|
||||
isJumpBackInCFREnabled = false,
|
||||
isRecentTabsFeatureEnabled = false,
|
||||
isTCPCFREnabled = false,
|
||||
isWallpaperOnboardingEnabled = false,
|
||||
tabsTrayRewriteEnabled = false,
|
||||
),
|
||||
) { it.activity }
|
||||
|
||||
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348361
|
||||
@SmokeTest
|
||||
@Test
|
||||
fun verifyFirefoxSuggestSponsoredSearchResultsTest() {
|
||||
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||
navigationToolbar {
|
||||
}.clickUrlbar {
|
||||
typeSearch(searchTerm = "Amazon")
|
||||
verifySearchEngineSuggestionResults(
|
||||
rule = activityTestRule,
|
||||
searchSuggestions = arrayOf(
|
||||
"Firefox Suggest",
|
||||
"Amazon.com - Official Site",
|
||||
"Sponsored",
|
||||
),
|
||||
searchTerm = "Amazon",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348362
|
||||
@Test
|
||||
fun verifyFirefoxSuggestSponsoredSearchResultsWithPartialKeywordTest() {
|
||||
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||
navigationToolbar {
|
||||
}.clickUrlbar {
|
||||
typeSearch(searchTerm = "Amaz")
|
||||
verifySearchEngineSuggestionResults(
|
||||
rule = activityTestRule,
|
||||
searchSuggestions = arrayOf(
|
||||
"Firefox Suggest",
|
||||
"Amazon.com - Official Site",
|
||||
"Sponsored",
|
||||
),
|
||||
searchTerm = "Amaz",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348363
|
||||
@Test
|
||||
fun openFirefoxSuggestSponsoredSearchResultsTest() {
|
||||
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||
navigationToolbar {
|
||||
}.clickUrlbar {
|
||||
typeSearch(searchTerm = "Amazon")
|
||||
verifySearchEngineSuggestionResults(
|
||||
rule = activityTestRule,
|
||||
searchSuggestions = arrayOf(
|
||||
"Firefox Suggest",
|
||||
"Amazon.com - Official Site",
|
||||
"Sponsored",
|
||||
),
|
||||
searchTerm = "Amazon",
|
||||
)
|
||||
}.clickSearchSuggestion("Amazon.com - Official Site") {
|
||||
waitForPageToLoad()
|
||||
verifyUrl(
|
||||
"amazon.com/?tag=admarketus-20&ref=pd_sl_924ab4435c5a5c23aa2804307ee0669ab36f88caee841ce51d1f2ecb&mfadid=adm",
|
||||
)
|
||||
verifyTabCounter("1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348369
|
||||
@Test
|
||||
fun verifyFirefoxSuggestSponsoredSearchResultsWithEditedKeywordTest() {
|
||||
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||
navigationToolbar {
|
||||
}.clickUrlbar {
|
||||
typeSearch(searchTerm = "Amazon")
|
||||
deleteSearchKeywordCharacters(numberOfDeletionSteps = 3)
|
||||
verifySearchEngineSuggestionResults(
|
||||
rule = activityTestRule,
|
||||
searchSuggestions = arrayOf(
|
||||
"Firefox Suggest",
|
||||
"Amazon.com - Official Site",
|
||||
"Sponsored",
|
||||
),
|
||||
searchTerm = "Amazon",
|
||||
shouldEditKeyword = true,
|
||||
numberOfDeletionSteps = 3,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348374
|
||||
@SmokeTest
|
||||
@Test
|
||||
fun verifyFirefoxSuggestNonSponsoredSearchResultsTest() {
|
||||
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||
navigationToolbar {
|
||||
}.clickUrlbar {
|
||||
typeSearch(searchTerm = "Marvel")
|
||||
verifySearchEngineSuggestionResults(
|
||||
rule = activityTestRule,
|
||||
searchSuggestions = arrayOf(
|
||||
"Firefox Suggest",
|
||||
"Wikipedia - Marvel Cinematic Universe",
|
||||
),
|
||||
searchTerm = "Marvel",
|
||||
)
|
||||
verifySuggestionsAreNotDisplayed(
|
||||
rule = activityTestRule,
|
||||
searchSuggestions = arrayOf(
|
||||
"Sponsored",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348375
|
||||
@Test
|
||||
fun verifyFirefoxSuggestNonSponsoredSearchResultsWithPartialKeywordTest() {
|
||||
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||
navigationToolbar {
|
||||
}.clickUrlbar {
|
||||
typeSearch(searchTerm = "Marv")
|
||||
verifySearchEngineSuggestionResults(
|
||||
rule = activityTestRule,
|
||||
searchSuggestions = arrayOf(
|
||||
"Firefox Suggest",
|
||||
"Wikipedia - Marvel Cinematic Universe",
|
||||
),
|
||||
searchTerm = "Marv",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348376
|
||||
@Test
|
||||
fun openFirefoxSuggestNonSponsoredSearchResultsTest() {
|
||||
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||
navigationToolbar {
|
||||
}.clickUrlbar {
|
||||
typeSearch(searchTerm = "Marvel")
|
||||
verifySearchEngineSuggestionResults(
|
||||
rule = activityTestRule,
|
||||
searchSuggestions = arrayOf(
|
||||
"Firefox Suggest",
|
||||
"Wikipedia - Marvel Cinematic Universe",
|
||||
),
|
||||
searchTerm = "Marvel",
|
||||
)
|
||||
}.clickSearchSuggestion("Wikipedia - Marvel Cinematic Universe") {
|
||||
waitForPageToLoad()
|
||||
verifyUrl(
|
||||
"wikipedia.org/wiki/Marvel_Cinematic_Universe",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
/* 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.ui
|
||||
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
|
||||
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
|
||||
import org.mozilla.fenix.helpers.TestAssetHelper.TestAsset
|
||||
import org.mozilla.fenix.helpers.TestAssetHelper.getGPCTestAsset
|
||||
import org.mozilla.fenix.ui.robots.homeScreen
|
||||
import org.mozilla.fenix.ui.robots.navigationToolbar
|
||||
|
||||
/**
|
||||
* Tests for Global Privacy Control setting.
|
||||
*/
|
||||
|
||||
class GlobalPrivacyControlTest {
|
||||
private lateinit var mockWebServer: MockWebServer
|
||||
private lateinit var gpcPage: TestAsset
|
||||
|
||||
@get:Rule
|
||||
val activityTestRule = HomeActivityIntentTestRule(
|
||||
isJumpBackInCFREnabled = false,
|
||||
isTCPCFREnabled = false,
|
||||
isWallpaperOnboardingEnabled = false,
|
||||
skipOnboarding = true,
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockWebServer = MockWebServer().apply {
|
||||
dispatcher = AndroidAssetDispatcher()
|
||||
start()
|
||||
}
|
||||
|
||||
gpcPage = getGPCTestAsset(mockWebServer)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockWebServer.shutdown()
|
||||
}
|
||||
|
||||
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2429327
|
||||
@Test
|
||||
fun testGPCinNormalBrowsing() {
|
||||
navigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(gpcPage.url) {
|
||||
verifyPageContent("GPC not enabled.")
|
||||
}.openThreeDotMenu {
|
||||
}.openSettings {
|
||||
}.openEnhancedTrackingProtectionSubMenu {
|
||||
scrollToGCPSettings()
|
||||
verifyGPCTextWithSwitchWidget()
|
||||
verifyGPCSwitchEnabled(false)
|
||||
switchGPCToggle()
|
||||
}.goBack {
|
||||
}.goBackToBrowser {
|
||||
verifyPageContent("GPC is enabled.")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2429364
|
||||
@Test
|
||||
fun testGPCinPrivateBrowsing() {
|
||||
homeScreen { }.togglePrivateBrowsingMode()
|
||||
navigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(gpcPage.url) {
|
||||
verifyPageContent("GPC is enabled.")
|
||||
}.openThreeDotMenu {
|
||||
}.openSettings {
|
||||
}.openEnhancedTrackingProtectionSubMenu {
|
||||
scrollToGCPSettings()
|
||||
verifyGPCTextWithSwitchWidget()
|
||||
verifyGPCSwitchEnabled(false)
|
||||
switchGPCToggle()
|
||||
}.goBack {
|
||||
}.goBackToBrowser {
|
||||
verifyPageContent("GPC is enabled.")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,119 +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.ui.robots
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.Visibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withChild
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withClassName
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.containsString
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.assertIsChecked
|
||||
import org.mozilla.fenix.helpers.atPosition
|
||||
import org.mozilla.fenix.helpers.click
|
||||
import org.mozilla.fenix.helpers.isChecked
|
||||
|
||||
/**
|
||||
* Implementation of Robot Pattern for the settings Delete Browsing Data On Quit sub menu.
|
||||
*/
|
||||
class SettingsSubMenuDeleteBrowsingDataOnQuitRobot {
|
||||
|
||||
fun verifyNavigationToolBarHeader() =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.navigationToolbar),
|
||||
withChild(withText(R.string.preferences_delete_browsing_data_on_quit)),
|
||||
),
|
||||
)
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
fun verifyDeleteBrowsingOnQuitEnabled(enabled: Boolean) =
|
||||
deleteBrowsingOnQuitButton.assertIsChecked(enabled)
|
||||
|
||||
fun verifyDeleteBrowsingOnQuitButtonSummary() =
|
||||
onView(
|
||||
withText(R.string.preference_summary_delete_browsing_data_on_quit_2),
|
||||
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
fun clickDeleteBrowsingOnQuitButtonSwitch() = onView(withResourceName("switch_widget")).click()
|
||||
|
||||
fun verifyAllTheCheckBoxesText() {
|
||||
openTabsCheckbox
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
browsingHistoryCheckbox
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
cookiesAndSiteDataCheckbox
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
onView(withText(R.string.preferences_delete_browsing_data_cookies_subtitle))
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
cachedFilesCheckbox
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
onView(withText(R.string.preferences_delete_browsing_data_cached_files_subtitle))
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
sitePermissionsCheckbox
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
}
|
||||
|
||||
fun verifyAllTheCheckBoxesChecked(checked: Boolean) {
|
||||
for (index in 2..7) {
|
||||
onView(withId(R.id.recycler_view))
|
||||
.check(
|
||||
matches(
|
||||
atPosition(
|
||||
index,
|
||||
hasDescendant(
|
||||
allOf(
|
||||
withResourceName(containsString("checkbox")),
|
||||
isChecked(checked),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Transition {
|
||||
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
|
||||
goBackButton.click()
|
||||
|
||||
SettingsRobot().interact()
|
||||
return SettingsRobot.Transition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val goBackButton = onView(withContentDescription("Navigate up"))
|
||||
|
||||
private val deleteBrowsingOnQuitButton =
|
||||
onView(withClassName(containsString("android.widget.Switch")))
|
||||
|
||||
private val openTabsCheckbox =
|
||||
onView(withText(R.string.preferences_delete_browsing_data_tabs_title_2))
|
||||
|
||||
private val browsingHistoryCheckbox =
|
||||
onView(withText(R.string.preferences_delete_browsing_data_browsing_history_title))
|
||||
|
||||
private val cookiesAndSiteDataCheckbox = onView(withText(R.string.preferences_delete_browsing_data_cookies_and_site_data))
|
||||
|
||||
private val cachedFilesCheckbox =
|
||||
onView(withText(R.string.preferences_delete_browsing_data_cached_files))
|
||||
|
||||
private val sitePermissionsCheckbox =
|
||||
onView(withText(R.string.preferences_delete_browsing_data_site_permissions))
|
||||
/* 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.ui.robots
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.Visibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withChild
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withClassName
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.containsString
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.assertIsChecked
|
||||
import org.mozilla.fenix.helpers.atPosition
|
||||
import org.mozilla.fenix.helpers.click
|
||||
import org.mozilla.fenix.helpers.isChecked
|
||||
|
||||
/**
|
||||
* Implementation of Robot Pattern for the settings Delete Browsing Data On Quit sub menu.
|
||||
*/
|
||||
class SettingsSubMenuDeleteBrowsingDataOnQuitRobot {
|
||||
|
||||
fun verifyNavigationToolBarHeader() =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.navigationToolbar),
|
||||
withChild(withText(R.string.preferences_delete_browsing_data_on_quit)),
|
||||
),
|
||||
)
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
fun verifyDeleteBrowsingOnQuitEnabled(enabled: Boolean) =
|
||||
deleteBrowsingOnQuitButton.assertIsChecked(enabled)
|
||||
|
||||
fun verifyDeleteBrowsingOnQuitButtonSummary() =
|
||||
onView(
|
||||
withText(R.string.preference_summary_delete_browsing_data_on_quit_2),
|
||||
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
fun clickDeleteBrowsingOnQuitButtonSwitch() = onView(withResourceName("switch_widget")).click()
|
||||
|
||||
fun verifyAllTheCheckBoxesText() {
|
||||
openTabsCheckbox
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
browsingHistoryCheckbox
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
cookiesAndSiteDataCheckbox
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
onView(withText(R.string.preferences_delete_browsing_data_cookies_subtitle))
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
cachedFilesCheckbox
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
onView(withText(R.string.preferences_delete_browsing_data_cached_files_subtitle))
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
sitePermissionsCheckbox
|
||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
}
|
||||
|
||||
fun verifyAllTheCheckBoxesChecked(checked: Boolean) {
|
||||
for (index in 2..7) {
|
||||
onView(withId(R.id.recycler_view))
|
||||
.check(
|
||||
matches(
|
||||
atPosition(
|
||||
index,
|
||||
hasDescendant(
|
||||
allOf(
|
||||
withResourceName(containsString("checkbox")),
|
||||
isChecked(checked),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Transition {
|
||||
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
|
||||
goBackButton.click()
|
||||
|
||||
SettingsRobot().interact()
|
||||
return SettingsRobot.Transition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val goBackButton = onView(withContentDescription("Navigate up"))
|
||||
|
||||
private val deleteBrowsingOnQuitButton =
|
||||
onView(withClassName(containsString("android.widget.Switch")))
|
||||
|
||||
private val openTabsCheckbox =
|
||||
onView(withText(R.string.preferences_delete_browsing_data_tabs_title_2))
|
||||
|
||||
private val browsingHistoryCheckbox =
|
||||
onView(withText(R.string.preferences_delete_browsing_data_browsing_history_title))
|
||||
|
||||
private val cookiesAndSiteDataCheckbox = onView(withText(R.string.preferences_delete_browsing_data_cookies_and_site_data))
|
||||
|
||||
private val cachedFilesCheckbox =
|
||||
onView(withText(R.string.preferences_delete_browsing_data_cached_files))
|
||||
|
||||
private val sitePermissionsCheckbox =
|
||||
onView(withText(R.string.preferences_delete_browsing_data_site_permissions))
|
||||
|
@ -1,124 +1,124 @@
|
||||
/* 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.ui.robots
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.uiautomator.UiSelector
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.containsString
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
|
||||
import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone
|
||||
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
|
||||
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
|
||||
import org.mozilla.fenix.helpers.TestHelper.mDevice
|
||||
import org.mozilla.fenix.helpers.TestHelper.packageName
|
||||
import org.mozilla.fenix.helpers.click
|
||||
|
||||
/**
|
||||
* Implementation of Robot Pattern for the settings Site Permissions Notification sub menu.
|
||||
*/
|
||||
class SettingsSubMenuSitePermissionsExceptionsRobot {
|
||||
fun verifyExceptionsEmptyList() {
|
||||
mDevice.findObject(UiSelector().text(getStringResource(R.string.no_site_exceptions)))
|
||||
.waitForExists(waitingTime)
|
||||
onView(withText(R.string.no_site_exceptions)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun verifyExceptionCreated(url: String, shouldBeDisplayed: Boolean) {
|
||||
if (shouldBeDisplayed) {
|
||||
exceptionsList.waitForExists(waitingTime)
|
||||
onView(withText(containsString(url))).check(matches(isDisplayed()))
|
||||
} else {
|
||||
assertUIObjectIsGone(itemContainingText(url))
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyClearPermissionsDialog() {
|
||||
onView(withText(R.string.clear_permissions)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.confirm_clear_permissions_on_all_sites)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.clear_permissions_positive)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.clear_permissions_negative)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
// Click button for resetting all of one site's permissions to default
|
||||
fun clickClearPermissionsForOneSite() {
|
||||
swipeToBottom()
|
||||
onView(withText(R.string.clear_permissions))
|
||||
.check(matches(isDisplayed()))
|
||||
.click()
|
||||
}
|
||||
fun verifyClearPermissionsForOneSiteDialog() {
|
||||
onView(withText(R.string.clear_permissions)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.confirm_clear_permissions_site)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.clear_permissions_positive)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.clear_permissions_negative)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun openSiteExceptionsDetails(url: String) {
|
||||
exceptionsList.waitForExists(waitingTime)
|
||||
onView(withText(containsString(url))).click()
|
||||
}
|
||||
|
||||
fun verifyPermissionSettingSummary(setting: String, summary: String) {
|
||||
onView(
|
||||
allOf(
|
||||
withText(setting),
|
||||
hasSibling(withText(summary)),
|
||||
),
|
||||
).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun openChangePermissionSettingsMenu(permissionSetting: String) {
|
||||
onView(withText(containsString(permissionSetting))).click()
|
||||
}
|
||||
|
||||
// Click button for resetting all permissions for all websites
|
||||
fun clickClearPermissionsOnAllSites() {
|
||||
exceptionsList.waitForExists(waitingTime)
|
||||
onView(withId(R.id.delete_all_site_permissions_button))
|
||||
.check(matches(isDisplayed()))
|
||||
.click()
|
||||
}
|
||||
|
||||
// Click button for resetting one site permission to default
|
||||
fun clickClearOnePermissionForOneSite() {
|
||||
onView(withText(R.string.clear_permission))
|
||||
.check(matches(isDisplayed()))
|
||||
.click()
|
||||
}
|
||||
|
||||
fun verifyResetPermissionDefaultForThisSiteDialog() {
|
||||
onView(withText(R.string.clear_permission)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.confirm_clear_permission_site)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.clear_permissions_positive)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.clear_permissions_negative)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun clickOK() = onView(withText(R.string.clear_permissions_positive)).click()
|
||||
|
||||
fun clickCancel() = onView(withText(R.string.clear_permissions_negative)).click()
|
||||
|
||||
class Transition {
|
||||
fun goBack(interact: SettingsSubMenuSitePermissionsRobot.() -> Unit): SettingsSubMenuSitePermissionsRobot.Transition {
|
||||
goBackButton().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsRobot.Transition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun goBackButton() =
|
||||
onView(allOf(withContentDescription("Navigate up")))
|
||||
|
||||
private val exceptionsList =
|
||||
mDevice.findObject(UiSelector().resourceId("$packageName:id/exceptions"))
|
||||
/* 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.ui.robots
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.uiautomator.UiSelector
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.containsString
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
|
||||
import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone
|
||||
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
|
||||
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
|
||||
import org.mozilla.fenix.helpers.TestHelper.mDevice
|
||||
import org.mozilla.fenix.helpers.TestHelper.packageName
|
||||
import org.mozilla.fenix.helpers.click
|
||||
|
||||
/**
|
||||
* Implementation of Robot Pattern for the settings Site Permissions Notification sub menu.
|
||||
*/
|
||||
class SettingsSubMenuSitePermissionsExceptionsRobot {
|
||||
fun verifyExceptionsEmptyList() {
|
||||
mDevice.findObject(UiSelector().text(getStringResource(R.string.no_site_exceptions)))
|
||||
.waitForExists(waitingTime)
|
||||
onView(withText(R.string.no_site_exceptions)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun verifyExceptionCreated(url: String, shouldBeDisplayed: Boolean) {
|
||||
if (shouldBeDisplayed) {
|
||||
exceptionsList.waitForExists(waitingTime)
|
||||
onView(withText(containsString(url))).check(matches(isDisplayed()))
|
||||
} else {
|
||||
assertUIObjectIsGone(itemContainingText(url))
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyClearPermissionsDialog() {
|
||||
onView(withText(R.string.clear_permissions)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.confirm_clear_permissions_on_all_sites)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.clear_permissions_positive)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.clear_permissions_negative)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
// Click button for resetting all of one site's permissions to default
|
||||
fun clickClearPermissionsForOneSite() {
|
||||
swipeToBottom()
|
||||
onView(withText(R.string.clear_permissions))
|
||||
.check(matches(isDisplayed()))
|
||||
.click()
|
||||
}
|
||||
fun verifyClearPermissionsForOneSiteDialog() {
|
||||
onView(withText(R.string.clear_permissions)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.confirm_clear_permissions_site)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.clear_permissions_positive)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.clear_permissions_negative)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun openSiteExceptionsDetails(url: String) {
|
||||
exceptionsList.waitForExists(waitingTime)
|
||||
onView(withText(containsString(url))).click()
|
||||
}
|
||||
|
||||
fun verifyPermissionSettingSummary(setting: String, summary: String) {
|
||||
onView(
|
||||
allOf(
|
||||
withText(setting),
|
||||
hasSibling(withText(summary)),
|
||||
),
|
||||
).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun openChangePermissionSettingsMenu(permissionSetting: String) {
|
||||
onView(withText(containsString(permissionSetting))).click()
|
||||
}
|
||||
|
||||
// Click button for resetting all permissions for all websites
|
||||
fun clickClearPermissionsOnAllSites() {
|
||||
exceptionsList.waitForExists(waitingTime)
|
||||
onView(withId(R.id.delete_all_site_permissions_button))
|
||||
.check(matches(isDisplayed()))
|
||||
.click()
|
||||
}
|
||||
|
||||
// Click button for resetting one site permission to default
|
||||
fun clickClearOnePermissionForOneSite() {
|
||||
onView(withText(R.string.clear_permission))
|
||||
.check(matches(isDisplayed()))
|
||||
.click()
|
||||
}
|
||||
|
||||
fun verifyResetPermissionDefaultForThisSiteDialog() {
|
||||
onView(withText(R.string.clear_permission)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.confirm_clear_permission_site)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.clear_permissions_positive)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.clear_permissions_negative)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun clickOK() = onView(withText(R.string.clear_permissions_positive)).click()
|
||||
|
||||
fun clickCancel() = onView(withText(R.string.clear_permissions_negative)).click()
|
||||
|
||||
class Transition {
|
||||
fun goBack(interact: SettingsSubMenuSitePermissionsRobot.() -> Unit): SettingsSubMenuSitePermissionsRobot.Transition {
|
||||
goBackButton().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsRobot.Transition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun goBackButton() =
|
||||
onView(allOf(withContentDescription("Navigate up")))
|
||||
|
||||
private val exceptionsList =
|
||||
mDevice.findObject(UiSelector().resourceId("$packageName:id/exceptions"))
|
||||
|
@ -1,199 +1,199 @@
|
||||
/* 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.ui.robots
|
||||
|
||||
import androidx.preference.R
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers.Visibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
|
||||
import org.mozilla.fenix.helpers.click
|
||||
|
||||
/**
|
||||
* Implementation of Robot Pattern for the settings Site Permissions sub menu.
|
||||
*/
|
||||
class SettingsSubMenuSitePermissionsRobot {
|
||||
|
||||
fun verifySitePermissionsToolbarTitle() =
|
||||
onView(withText("Site permissions")).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
fun verifyToolbarGoBackButton() =
|
||||
goBackButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
fun verifySitePermissionOption(option: String, summary: String = "") {
|
||||
scrollToElementByText(option)
|
||||
onView(
|
||||
allOf(
|
||||
withText(option),
|
||||
hasSibling(withText(summary)),
|
||||
),
|
||||
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
}
|
||||
|
||||
class Transition {
|
||||
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
|
||||
goBackButton().click()
|
||||
|
||||
SettingsRobot().interact()
|
||||
return SettingsRobot.Transition()
|
||||
}
|
||||
|
||||
fun openAutoPlay(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Autoplay")),
|
||||
),
|
||||
)
|
||||
|
||||
openAutoPlay().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openCamera(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Camera")),
|
||||
),
|
||||
)
|
||||
|
||||
openCamera().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openLocation(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Location")),
|
||||
),
|
||||
)
|
||||
|
||||
openLocation().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openMicrophone(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Microphone")),
|
||||
),
|
||||
)
|
||||
|
||||
openMicrophone().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openNotification(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Notification")),
|
||||
),
|
||||
)
|
||||
|
||||
openNotification().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openPersistentStorage(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Persistent Storage")),
|
||||
),
|
||||
)
|
||||
|
||||
openPersistentStorage().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openDRMControlledContent(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("DRM-controlled content")),
|
||||
),
|
||||
)
|
||||
|
||||
openDrmControlledContent().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openExceptions(
|
||||
interact: SettingsSubMenuSitePermissionsExceptionsRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsExceptionsRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Exceptions")),
|
||||
),
|
||||
)
|
||||
|
||||
openExceptions().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsExceptionsRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsExceptionsRobot.Transition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun goBackButton() =
|
||||
onView(withContentDescription("Navigate up"))
|
||||
|
||||
private fun openAutoPlay() =
|
||||
onView(allOf(withText("Autoplay")))
|
||||
|
||||
private fun openCamera() =
|
||||
onView(allOf(withText("Camera")))
|
||||
|
||||
private fun openLocation() =
|
||||
onView(allOf(withText("Location")))
|
||||
|
||||
private fun openMicrophone() =
|
||||
onView(allOf(withText("Microphone")))
|
||||
|
||||
private fun openNotification() =
|
||||
onView(allOf(withText("Notification")))
|
||||
|
||||
private fun openPersistentStorage() =
|
||||
onView(allOf(withText("Persistent Storage")))
|
||||
|
||||
private fun openDrmControlledContent() =
|
||||
onView(allOf(withText("DRM-controlled content")))
|
||||
|
||||
private fun openExceptions() =
|
||||
onView(allOf(withText("Exceptions")))
|
||||
/* 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.ui.robots
|
||||
|
||||
import androidx.preference.R
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers.Visibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
|
||||
import org.mozilla.fenix.helpers.click
|
||||
|
||||
/**
|
||||
* Implementation of Robot Pattern for the settings Site Permissions sub menu.
|
||||
*/
|
||||
class SettingsSubMenuSitePermissionsRobot {
|
||||
|
||||
fun verifySitePermissionsToolbarTitle() =
|
||||
onView(withText("Site permissions")).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
fun verifyToolbarGoBackButton() =
|
||||
goBackButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
|
||||
fun verifySitePermissionOption(option: String, summary: String = "") {
|
||||
scrollToElementByText(option)
|
||||
onView(
|
||||
allOf(
|
||||
withText(option),
|
||||
hasSibling(withText(summary)),
|
||||
),
|
||||
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
}
|
||||
|
||||
class Transition {
|
||||
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
|
||||
goBackButton().click()
|
||||
|
||||
SettingsRobot().interact()
|
||||
return SettingsRobot.Transition()
|
||||
}
|
||||
|
||||
fun openAutoPlay(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Autoplay")),
|
||||
),
|
||||
)
|
||||
|
||||
openAutoPlay().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openCamera(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Camera")),
|
||||
),
|
||||
)
|
||||
|
||||
openCamera().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openLocation(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Location")),
|
||||
),
|
||||
)
|
||||
|
||||
openLocation().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openMicrophone(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Microphone")),
|
||||
),
|
||||
)
|
||||
|
||||
openMicrophone().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openNotification(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Notification")),
|
||||
),
|
||||
)
|
||||
|
||||
openNotification().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openPersistentStorage(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Persistent Storage")),
|
||||
),
|
||||
)
|
||||
|
||||
openPersistentStorage().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openDRMControlledContent(
|
||||
interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsCommonRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("DRM-controlled content")),
|
||||
),
|
||||
)
|
||||
|
||||
openDrmControlledContent().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsCommonRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsCommonRobot.Transition()
|
||||
}
|
||||
|
||||
fun openExceptions(
|
||||
interact: SettingsSubMenuSitePermissionsExceptionsRobot.() -> Unit,
|
||||
): SettingsSubMenuSitePermissionsExceptionsRobot.Transition {
|
||||
onView(withId(R.id.recycler_view)).perform(
|
||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText("Exceptions")),
|
||||
),
|
||||
)
|
||||
|
||||
openExceptions().click()
|
||||
|
||||
SettingsSubMenuSitePermissionsExceptionsRobot().interact()
|
||||
return SettingsSubMenuSitePermissionsExceptionsRobot.Transition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun goBackButton() =
|
||||
onView(withContentDescription("Navigate up"))
|
||||
|
||||
private fun openAutoPlay() =
|
||||
onView(allOf(withText("Autoplay")))
|
||||
|
||||
private fun openCamera() =
|
||||
onView(allOf(withText("Camera")))
|
||||
|
||||
private fun openLocation() =
|
||||
onView(allOf(withText("Location")))
|
||||
|
||||
private fun openMicrophone() =
|
||||
onView(allOf(withText("Microphone")))
|
||||
|
||||
private fun openNotification() =
|
||||
onView(allOf(withText("Notification")))
|
||||
|
||||
private fun openPersistentStorage() =
|
||||
onView(allOf(withText("Persistent Storage")))
|
||||
|
||||
private fun openDrmControlledContent() =
|
||||
onView(allOf(withText("DRM-controlled content")))
|
||||
|
||||
private fun openExceptions() =
|
||||
onView(allOf(withText("Exceptions")))
|
||||
|
@ -0,0 +1,70 @@
|
||||
/* 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.components
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
|
||||
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.ALLOW_ADDITIONAL_HEADERS
|
||||
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE
|
||||
import mozilla.components.concept.engine.request.RequestInterceptor
|
||||
|
||||
/**
|
||||
* [RequestInterceptor] implementation for intercepting URL load requests to allow custom
|
||||
* behaviour.
|
||||
*
|
||||
* @param isDeviceRamAboveThreshold Whether or not the device ram is above a threshold.
|
||||
*/
|
||||
class UrlRequestInterceptor(private val isDeviceRamAboveThreshold: Boolean) : RequestInterceptor {
|
||||
|
||||
private val isGoogleSearchRequest by lazy {
|
||||
Regex("^https://www\\.google\\.(?:.+)/search")
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun getAdditionalHeaders(isDeviceRamAboveThreshold: Boolean): Map<String, String> {
|
||||
val value = if (isDeviceRamAboveThreshold) {
|
||||
"1"
|
||||
} else {
|
||||
"0"
|
||||
}
|
||||
|
||||
return mapOf(
|
||||
"X-Search-Subdivision" to value,
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun shouldInterceptRequest(
|
||||
uri: String,
|
||||
isSubframeRequest: Boolean,
|
||||
): Boolean {
|
||||
return !isSubframeRequest && isGoogleSearchRequest.containsMatchIn(uri)
|
||||
}
|
||||
|
||||
override fun onLoadRequest(
|
||||
engineSession: EngineSession,
|
||||
uri: String,
|
||||
lastUri: String?,
|
||||
hasUserGesture: Boolean,
|
||||
isSameDomain: Boolean,
|
||||
isRedirect: Boolean,
|
||||
isDirectNavigation: Boolean,
|
||||
isSubframeRequest: Boolean,
|
||||
): RequestInterceptor.InterceptionResponse? {
|
||||
if (!shouldInterceptRequest(uri = uri, isSubframeRequest = isSubframeRequest)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return RequestInterceptor.InterceptionResponse.Url(
|
||||
url = uri,
|
||||
flags = LoadUrlFlags.select(
|
||||
LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE,
|
||||
ALLOW_ADDITIONAL_HEADERS,
|
||||
),
|
||||
additionalHeaders = getAdditionalHeaders(isDeviceRamAboveThreshold),
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,212 @@
|
||||
/* 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.components.metrics.fonts
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.fonts.Font
|
||||
import android.graphics.fonts.SystemFonts
|
||||
import android.os.Build
|
||||
import android.os.LocaleList
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import org.mozilla.fenix.Config
|
||||
import org.mozilla.fenix.GleanMetrics.Metrics
|
||||
import org.mozilla.fenix.GleanMetrics.Pings
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
/**
|
||||
* Parse all of the fonts on the user's phone, then put them into the
|
||||
* `font_list_json` Metric to be submitted via Telemetry later.
|
||||
*/
|
||||
class FontEnumerationWorker(
|
||||
context: Context,
|
||||
workerParameters: WorkerParameters,
|
||||
) : CoroutineWorker(context, workerParameters) {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
||||
val s: String
|
||||
try {
|
||||
readAllFonts()
|
||||
s = createJSONString()
|
||||
} catch (e: Exception) {
|
||||
return@withContext Result.retry()
|
||||
}
|
||||
|
||||
Metrics.fontListJson.set(s)
|
||||
Pings.fontList.submit()
|
||||
|
||||
// To avoid getting multiple submissions from new installs, set directly
|
||||
// to the desired number of submissions
|
||||
applicationContext.settings().numFontListSent = kDesiredSubmissions
|
||||
|
||||
return@withContext Result.success()
|
||||
}
|
||||
|
||||
private val brokenFonts: ArrayList<Pair<String, String>> = ArrayList()
|
||||
private val fonts: MutableSet<FontMetric> = HashSet()
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun readAllFonts() {
|
||||
for (path in getSystemFonts()) {
|
||||
try {
|
||||
fonts.add(FontParser.parse(path))
|
||||
} catch (e: Exception) {
|
||||
brokenFonts.add(Pair(path, FontParser.calculateFileHash(path)))
|
||||
}
|
||||
}
|
||||
for (path in getAPIFonts()) {
|
||||
try {
|
||||
fonts.add(FontParser.parse(path))
|
||||
} catch (e: Exception) {
|
||||
brokenFonts.add(Pair(path, FontParser.calculateFileHash(path)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function creates a single JSON String containing
|
||||
* The user's phone information, as well as all the fonts and their information,
|
||||
* And the names of files that encountered a parsing error.
|
||||
*/
|
||||
@Throws(JSONException::class)
|
||||
fun createJSONString(): String {
|
||||
val submission = JSONObject()
|
||||
|
||||
run {
|
||||
submission.put("submission", kDesiredSubmissions)
|
||||
submission.put("brand", Build.BRAND)
|
||||
submission.put("device", Build.DEVICE)
|
||||
submission.put("hardware", Build.HARDWARE)
|
||||
submission.put("manufacturer", Build.MANUFACTURER)
|
||||
submission.put("model", Build.MODEL)
|
||||
submission.put("product", Build.PRODUCT)
|
||||
submission.put("release_version", Build.VERSION.RELEASE)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
submission.put("security_patch", Build.VERSION.SECURITY_PATCH)
|
||||
submission.put("base_os", Build.VERSION.BASE_OS)
|
||||
} else {
|
||||
submission.put("security_patch", "too-low-version")
|
||||
submission.put("base_os", "too-low-version")
|
||||
}
|
||||
val config: Configuration = this.applicationContext.resources.configuration
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val supportedLocales: LocaleList = LocaleList.getDefault()
|
||||
val sb = StringBuilder()
|
||||
for (i in 0 until supportedLocales.size()) {
|
||||
val locale: Locale = supportedLocales.get(i)
|
||||
sb.append(locale.toString())
|
||||
sb.append(",")
|
||||
}
|
||||
submission.put("current_locale", config.locales[0].toString())
|
||||
submission.put("all_locales", sb.toString())
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
submission.put("current_locale", config.locale.toString())
|
||||
submission.put("all_locales", "too-low-version")
|
||||
}
|
||||
}
|
||||
|
||||
val fontArr = JSONArray()
|
||||
for (fontDetails in fonts) {
|
||||
fontArr.put(fontDetails.toJson())
|
||||
}
|
||||
|
||||
val errorArr = JSONArray()
|
||||
for (error in brokenFonts) {
|
||||
val errorObj = JSONObject()
|
||||
errorObj.put("path", error.first)
|
||||
errorObj.put("hash", error.second)
|
||||
errorArr.put(errorObj)
|
||||
}
|
||||
|
||||
submission.put("fonts", fontArr)
|
||||
submission.put("errors", errorArr)
|
||||
|
||||
return submission.toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FONT_ENUMERATOR_WORK_NAME = "org.mozilla.fenix.metrics.font.work"
|
||||
private val HOUR_MILLIS: Long = 1.hours.inWholeMilliseconds
|
||||
private const val SIX: Long = 6
|
||||
|
||||
/**
|
||||
* Schedules the Activated User event if needed.
|
||||
*/
|
||||
fun sendActivatedSignalIfNeeded(context: Context) {
|
||||
val instanceWorkManager = WorkManager.getInstance(context)
|
||||
|
||||
if (!Config.channel.isNightlyOrDebug) {
|
||||
return
|
||||
}
|
||||
|
||||
if (context.settings().numFontListSent >= kDesiredSubmissions) {
|
||||
return
|
||||
}
|
||||
|
||||
val fontEnumeratorWork =
|
||||
OneTimeWorkRequest.Builder(FontEnumerationWorker::class.java)
|
||||
.setInitialDelay(HOUR_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, SIX, TimeUnit.HOURS)
|
||||
.build()
|
||||
|
||||
instanceWorkManager.beginUniqueWork(
|
||||
FONT_ENUMERATOR_WORK_NAME,
|
||||
ExistingWorkPolicy.KEEP,
|
||||
fontEnumeratorWork,
|
||||
).enqueue()
|
||||
}
|
||||
|
||||
private fun getSystemFonts(): ArrayList<String> {
|
||||
val file = File("/system/fonts")
|
||||
val ff: Array<out File>? = file.listFiles()
|
||||
val systemFonts: ArrayList<String> = ArrayList()
|
||||
if (ff != null) {
|
||||
for (f in ff) {
|
||||
systemFonts.add(f.absolutePath)
|
||||
}
|
||||
}
|
||||
return systemFonts
|
||||
}
|
||||
|
||||
private fun getAPIFonts(): List<String> {
|
||||
val aPIFonts: List<String>
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
aPIFonts = emptyList()
|
||||
} else {
|
||||
aPIFonts = ArrayList()
|
||||
val apiFonts: Set<Font> = SystemFonts.getAvailableFonts()
|
||||
for (f in apiFonts) {
|
||||
f.file?.let {
|
||||
aPIFonts.add(it.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
return aPIFonts
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of font submissions we would like from a user.
|
||||
* We will increment this number by one (via a code patch) when
|
||||
* we wish to perform another data collection effort on the Nightly
|
||||
* population.
|
||||
*/
|
||||
const val kDesiredSubmissions: Int = 4
|
||||
}
|
||||
}
|
@ -0,0 +1,253 @@
|
||||
/* 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.components.metrics.fonts
|
||||
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.DataInputStream
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* FontMetric represents the information about a Font File
|
||||
*/
|
||||
data class FontMetric(
|
||||
val path: String = "",
|
||||
val hash: String = "",
|
||||
) {
|
||||
var family: String = ""
|
||||
var subFamily: String = ""
|
||||
var uniqueSubFamily: String = ""
|
||||
var fullName: String = ""
|
||||
var fontVersion: String = ""
|
||||
var revision: Int = -1
|
||||
var created: Long = -1L
|
||||
var modified: Long = -1L
|
||||
|
||||
/**
|
||||
* Return a JSONObject of this Font's details
|
||||
*/
|
||||
fun toJson(): JSONObject {
|
||||
val jsonObject = JSONObject()
|
||||
try {
|
||||
// Use abbreviations to make the json smaller
|
||||
jsonObject.put("F", family.replace("\u0000", ""))
|
||||
jsonObject.put("SF", subFamily.replace("\u0000", ""))
|
||||
jsonObject.put("USF", uniqueSubFamily.replace("\u0000", ""))
|
||||
jsonObject.put("FN", fullName.replace("\u0000", ""))
|
||||
jsonObject.put("V", fontVersion.replace("\u0000", ""))
|
||||
jsonObject.put("R", revision)
|
||||
jsonObject.put("C", created)
|
||||
jsonObject.put("M", modified)
|
||||
jsonObject.put("H", hash)
|
||||
jsonObject.put("P", path.replace("\u0000", ""))
|
||||
} catch (_: JSONException) {
|
||||
}
|
||||
return jsonObject
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a font, given via an InputStream, to extract the Font information
|
||||
* including Family, SubFamily, Revision, etc
|
||||
*/
|
||||
object FontParser {
|
||||
/**
|
||||
* Parse a font file and return a FontMetric object describing it.
|
||||
* These functions are very similar, because this one is used in
|
||||
* real devices, the other in unit tests. Outside tests, the
|
||||
* FileInputStream does not support the reset() method
|
||||
*/
|
||||
fun parse(path: String): FontMetric {
|
||||
val hash = calculateFileHash(FileInputStream(path))
|
||||
val fontDetails = FontMetric(path, hash)
|
||||
|
||||
readFontFile(FileInputStream(path), fontDetails)
|
||||
|
||||
return fontDetails
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a font file and return a FontMetric object describing it
|
||||
*/
|
||||
fun parse(path: String, inputStream: InputStream): FontMetric {
|
||||
val hash = calculateFileHash(inputStream)
|
||||
val fontDetails = FontMetric(path, hash)
|
||||
|
||||
inputStream.reset()
|
||||
readFontFile(inputStream, fontDetails)
|
||||
|
||||
return fontDetails
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun readFontFile(inputStream: InputStream, fontDetails: FontMetric) {
|
||||
val file = DataInputStream(inputStream)
|
||||
val numFonts: Int
|
||||
val magicNumber = file.readInt()
|
||||
var bytesReadSoFar = 4
|
||||
|
||||
if (magicNumber == 0x74746366) {
|
||||
// The Font File has a TTC Header
|
||||
val majorVersion = file.readUnsignedShort()
|
||||
file.skipBytes(2) // Minor Version
|
||||
numFonts = file.readInt()
|
||||
bytesReadSoFar += 8
|
||||
|
||||
file.skipBytes(4 * numFonts) // OffsetTable
|
||||
bytesReadSoFar += 4 * numFonts
|
||||
if (majorVersion == 2) {
|
||||
file.skipBytes(12)
|
||||
bytesReadSoFar += 12
|
||||
}
|
||||
|
||||
file.skipBytes(4) // Magic Number for the Font
|
||||
bytesReadSoFar += 4
|
||||
}
|
||||
val numTables: Int = file.readUnsignedShort()
|
||||
bytesReadSoFar += 2
|
||||
file.skipBytes(6) // Rest of header
|
||||
bytesReadSoFar += 6
|
||||
|
||||
// Find the head table
|
||||
var headOffset = 0
|
||||
var nameOffset = 0
|
||||
var nameLength = 0
|
||||
for (i in 0 until numTables) {
|
||||
val tableName =
|
||||
CharArray(4) {
|
||||
file.readUnsignedByte().toChar()
|
||||
}
|
||||
file.skipBytes(4) // checksum
|
||||
val offset = file.readInt() // technically it's unsigned but we should be okay
|
||||
val length = file.readInt() // technically it's unsigned but we should be okay
|
||||
|
||||
bytesReadSoFar += 16
|
||||
|
||||
if (String(tableName) == "head") {
|
||||
headOffset = offset
|
||||
} else if (String(tableName) == "name") {
|
||||
nameOffset = offset
|
||||
nameLength = length
|
||||
}
|
||||
}
|
||||
|
||||
if (headOffset == 0 || nameOffset == 0) {
|
||||
throw IOException("Could not find head or name table")
|
||||
}
|
||||
|
||||
if (headOffset < nameOffset) {
|
||||
file.skipBytes(headOffset - bytesReadSoFar)
|
||||
bytesReadSoFar = headOffset
|
||||
bytesReadSoFar += readHeadTable(file, fontDetails)
|
||||
file.skipBytes(nameOffset - bytesReadSoFar)
|
||||
readNameTable(file, nameLength, fontDetails)
|
||||
} else {
|
||||
file.skipBytes(nameOffset - bytesReadSoFar)
|
||||
bytesReadSoFar = nameOffset
|
||||
bytesReadSoFar += readNameTable(file, nameLength, fontDetails)
|
||||
file.skipBytes(headOffset - bytesReadSoFar)
|
||||
readHeadTable(file, fontDetails)
|
||||
}
|
||||
file.close()
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun readHeadTable(file: DataInputStream, fontDetails: FontMetric): Int {
|
||||
// Find the details in the head table
|
||||
file.skipBytes(4) // Fixed version
|
||||
fontDetails.revision = file.readInt()
|
||||
file.skipBytes(12) // checksum, magic, flags, units
|
||||
fontDetails.created = file.readLong()
|
||||
fontDetails.modified = file.readLong()
|
||||
return 36
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun readNameTable(
|
||||
file: DataInputStream,
|
||||
tableLength: Int,
|
||||
fontDetails: FontMetric,
|
||||
): Int {
|
||||
file.skipBytes(2) // format
|
||||
val numNames = file.readUnsignedShort()
|
||||
val stringOffset = file.readUnsignedShort()
|
||||
var bytesReadSoFar = 6
|
||||
val nameTable = arrayListOf<Triple<Int, Int, Int>>()
|
||||
|
||||
for (i in 0 until numNames) {
|
||||
file.skipBytes(6) // platform id, encoding id, langid
|
||||
val nameID = file.readUnsignedShort()
|
||||
val length = file.readUnsignedShort()
|
||||
val offset = file.readUnsignedShort()
|
||||
nameTable.add(Triple(nameID, length, offset))
|
||||
bytesReadSoFar += 12
|
||||
}
|
||||
|
||||
val stringTableSize = min(tableLength - bytesReadSoFar, tableLength - stringOffset)
|
||||
val stringTable = ByteArray(stringTableSize)
|
||||
|
||||
if (stringTable.size != file.read(stringTable)) {
|
||||
throw IOException("Did not read entire string table")
|
||||
}
|
||||
|
||||
bytesReadSoFar += stringTable.size
|
||||
|
||||
// Now we're at the beginning of the string table
|
||||
for (i in nameTable) {
|
||||
when (i.first) {
|
||||
1 -> fontDetails.family = getString(stringTable, i.third, i.second)
|
||||
2 -> fontDetails.subFamily = getString(stringTable, i.third, i.second)
|
||||
3 -> fontDetails.uniqueSubFamily = getString(stringTable, i.third, i.second)
|
||||
4 -> fontDetails.fullName = getString(stringTable, i.third, i.second)
|
||||
5 -> fontDetails.fontVersion = getString(stringTable, i.third, i.second)
|
||||
}
|
||||
}
|
||||
return bytesReadSoFar
|
||||
}
|
||||
|
||||
private fun getString(
|
||||
stringTable: ByteArray,
|
||||
offset: Int,
|
||||
length: Int,
|
||||
): String {
|
||||
return String(stringTable.copyOfRange(offset, offset + length))
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the SHA-256 hash of the file passed
|
||||
*/
|
||||
fun calculateFileHash(path: String): String {
|
||||
return calculateFileHash(FileInputStream(path))
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the SHA-256 hash of the InputStream passed
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
private fun calculateFileHash(inputStream: InputStream): String {
|
||||
try {
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
val buffer = ByteArray(8192)
|
||||
var bytesRead: Int
|
||||
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||
md.update(buffer, 0, bytesRead)
|
||||
}
|
||||
val digest = md.digest()
|
||||
// Convert the byte array to a hexadecimal string
|
||||
val hashBuilder = StringBuilder()
|
||||
for (b in digest) {
|
||||
hashBuilder.append(String.format("%02X", b))
|
||||
}
|
||||
return hashBuilder.toString()
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
return "sha-256-not-found"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/* 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.components.toolbar
|
||||
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
|
||||
/**
|
||||
* An abstraction for the Toolbar Redesign feature.
|
||||
*/
|
||||
interface RedesignToolbarFeature {
|
||||
|
||||
/**
|
||||
* Returns true if the toolbar redesign feature is enabled.
|
||||
*/
|
||||
val isEnabled: Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The complete portions of the redesigned Toolbar ready for Nightly.
|
||||
*
|
||||
*/
|
||||
class CompleteRedesignToolbarFeature(
|
||||
private val settings: Settings,
|
||||
) : RedesignToolbarFeature {
|
||||
|
||||
override val isEnabled: Boolean
|
||||
get() = settings.enableRedesignToolbar
|
||||
}
|
||||
|
||||
/**
|
||||
* The incomplete portions of the redesigned Toolbar still in progress.
|
||||
*
|
||||
*/
|
||||
class IncompleteRedesignToolbarFeature(
|
||||
private val settings: Settings,
|
||||
) : RedesignToolbarFeature {
|
||||
|
||||
override val isEnabled: Boolean
|
||||
get() = settings.enableIncompleteToolbarRedesign
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/* 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.debugsettings.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* [DataStore] for accessing debugging settings.
|
||||
*/
|
||||
private val Context.debugSettings: DataStore<Preferences> by preferencesDataStore(name = "debug_settings")
|
||||
|
||||
private val debugDrawerEnabledKey = booleanPreferencesKey("debug_drawer_enabled")
|
||||
|
||||
/**
|
||||
* Cache for accessing any settings related to debugging.
|
||||
*/
|
||||
interface DebugSettingsRepository {
|
||||
|
||||
/**
|
||||
* [Flow] for checking whether the Debug Drawer is enabled.
|
||||
*/
|
||||
val debugDrawerEnabled: Flow<Boolean>
|
||||
|
||||
/**
|
||||
* Updates whether the debug drawer is enabled.
|
||||
*
|
||||
* @param enabled Whether the debug drawer is enabled.
|
||||
*/
|
||||
fun setDebugDrawerEnabled(enabled: Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of [DebugSettingsRepository].
|
||||
*
|
||||
* @param context Android context used to obtain the underlying [DataStore].
|
||||
* @param dataStore [DataStore] for accessing debugging settings.
|
||||
* @param writeScope [CoroutineScope] used for writing settings changes to disk.
|
||||
*/
|
||||
class DefaultDebugSettingsRepository(
|
||||
context: Context,
|
||||
private val dataStore: DataStore<Preferences> = context.debugSettings,
|
||||
private val writeScope: CoroutineScope,
|
||||
) : DebugSettingsRepository {
|
||||
|
||||
override val debugDrawerEnabled: Flow<Boolean> =
|
||||
dataStore.data.map { preferences ->
|
||||
preferences[debugDrawerEnabledKey] ?: false
|
||||
}
|
||||
|
||||
override fun setDebugDrawerEnabled(enabled: Boolean) {
|
||||
writeScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[debugDrawerEnabledKey] = enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/* 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.debugsettings.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Snackbar
|
||||
import androidx.compose.material.SnackbarHost
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.compose.button.FloatingActionButton
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Overlay for presenting Fenix-wide debugging content.
|
||||
*/
|
||||
@Composable
|
||||
fun DebugOverlay() {
|
||||
val snackbarState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
FloatingActionButton(
|
||||
icon = painterResource(R.drawable.ic_debug_transparent_fire_24),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.padding(start = 16.dp),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
snackbarState.showSnackbar("Show debug drawer")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// This must be the last element in the Box
|
||||
SnackbarHost(
|
||||
hostState = snackbarState,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
) { snackbarData ->
|
||||
Snackbar(
|
||||
snackbarData = snackbarData,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@LightDarkPreview
|
||||
private fun DebugOverlayPreview() {
|
||||
FirefoxTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = FirefoxTheme.colors.layer1),
|
||||
) {
|
||||
DebugOverlay()
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue