Merge remote-tracking branch 'origin/fenix/122.0' into iceraven

pull/745/head
akliuxingyuan 4 months ago
commit 279f9e954c

@ -1 +1 @@
Subproject commit 81883d4e4f0f81a00b67875a5f592480b898f3ce
Subproject commit a2934ef2d89153e3f7842dcfe71fa81af9888d68

@ -198,6 +198,7 @@ android {
buildFeatures {
viewBinding true
buildConfig true
}
androidResources {
@ -273,6 +274,9 @@ android {
excludes += ['META-INF/atomicfu.kotlin_module', 'META-INF/AL2.0', 'META-INF/LGPL2.1',
'META-INF/LICENSE.md', 'META-INF/LICENSE-notice.md']
}
jniLibs {
useLegacyPackaging true
}
}
@ -780,14 +784,14 @@ if (project.hasProperty("coverage")) {
def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*',
'**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*']
def kotlinDebugTree = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/${variant.name}", excludes: fileFilter)
def javaDebugTree = fileTree(dir: "$project.buildDir/intermediates/classes/${variant.flavorName}/${variant.buildType.name}",
def kotlinDebugTree = fileTree(dir: "$project.layout.buildDirectory/tmp/kotlin-classes/${variant.name}", excludes: fileFilter)
def javaDebugTree = fileTree(dir: "$project.layout.buildDirectory/intermediates/classes/${variant.flavorName}/${variant.buildType.name}",
excludes: fileFilter)
def mainSrc = "$project.projectDir/src/main/java"
sourceDirectories.setFrom(files([mainSrc]))
classDirectories.setFrom(files([kotlinDebugTree, javaDebugTree]))
executionData.setFrom(fileTree(dir: project.buildDir, includes: [
executionData.setFrom(fileTree(dir: project.layout.buildDirectory, includes: [
"jacoco/test${variant.name.capitalize()}UnitTest.exec",
'outputs/code-coverage/connected/*coverage.ec'
]))

@ -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)

@ -376,6 +376,27 @@ events:
metadata:
tags:
- PrivateBrowsing
opened_ext_pdf:
type: event
description: |
A user opened a PDF with Fenix from another app
extra_keys:
referrer_is_fenix:
description: |
If the PDF was opened from Fenix itself (for example from the Download notification)
type: boolean
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1871548
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/4940
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- mcastelluccio@mozilla.com
- calixte@mozilla.com
- sylvestre@mozilla.com
expires: never
synced_tab_opened:
type: event
description: |
@ -468,6 +489,39 @@ events:
notification_emails:
- android-probes@mozilla.com
expires: never
browser_toolbar_qr_scan_tapped:
type: event
description: |
An event that indicates that a user has tapped
QR scan button on browser toolbar.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1862096
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1862096
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: never
metadata:
tags:
- Toolbar
toolbar_tab_swipe:
type: event
description: |
A user swiped the toolbar to change the current tab.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1862096
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1862096
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: never
metadata:
tags:
- Toolbar
tab_view_changed:
type: event
description: |
@ -1033,7 +1087,7 @@ onboarding:
set_to_default_card:
type: event
description: |
User viewed juno onboarding set to default card.
User viewed onboarding set to default card.
extra_keys:
element_type:
type: string
@ -1067,7 +1121,7 @@ onboarding:
sign_in_card:
type: event
description: |
User viewed juno onboarding sign in card.
User viewed onboarding sign in card.
extra_keys:
element_type:
type: string
@ -1101,7 +1155,7 @@ onboarding:
turn_on_notifications_card:
type: event
description: |
User viewed juno onboarding notification permission card.
User viewed onboarding notification permission card.
extra_keys:
element_type:
type: string
@ -1135,7 +1189,7 @@ onboarding:
set_to_default:
type: event
description: |
User tapped on set to default button in juno onboarding.
User tapped on set to default button in onboarding.
extra_keys:
element_type:
type: string
@ -1169,7 +1223,7 @@ onboarding:
skip_default:
type: event
description: |
User tapped on skip set to default button in juno onboarding.
User tapped on skip set to default button in onboarding.
extra_keys:
element_type:
type: string
@ -1203,7 +1257,7 @@ onboarding:
sign_in:
type: event
description: |
User tapped on sign in button in juno onboarding.
User tapped on sign in button in onboarding.
extra_keys:
element_type:
type: string
@ -1237,7 +1291,7 @@ onboarding:
skip_sign_in:
type: event
description: |
User tapped on skip sign in button in juno onboarding.
User tapped on skip sign in button in onboarding.
extra_keys:
element_type:
type: string
@ -1271,7 +1325,7 @@ onboarding:
turn_on_notifications:
type: event
description: |
User tapped on turn on notifications button in juno onboarding.
User tapped on turn on notifications button in onboarding.
extra_keys:
element_type:
type: string
@ -1305,7 +1359,7 @@ onboarding:
skip_turn_on_notifications:
type: event
description: |
User tapped on skip turn on notification button in juno onboarding.
User tapped on skip turn on notification button in onboarding.
extra_keys:
element_type:
type: string
@ -1339,7 +1393,7 @@ onboarding:
add_search_widget_card:
type: event
description: |
User viewed juno onboarding add search widget card.
User viewed onboarding add search widget card.
extra_keys:
element_type:
type: string
@ -1373,7 +1427,7 @@ onboarding:
add_search_widget:
type: event
description: |
User tapped on Add Firefox Widget in juno onboarding.
User tapped on Add Firefox Widget in onboarding.
extra_keys:
element_type:
type: string
@ -1407,7 +1461,7 @@ onboarding:
skip_add_search_widget:
type: event
description: |
User tapped on skip add search widget button in juno onboarding.
User tapped on skip add search widget button in onboarding.
extra_keys:
element_type:
type: string
@ -1441,7 +1495,7 @@ onboarding:
privacy_policy:
type: event
description: |
User tapped on privacy policy link in juno onboarding.
User tapped on privacy policy link in onboarding.
extra_keys:
element_type:
type: string
@ -1475,7 +1529,7 @@ onboarding:
completed:
type: event
description: |
User completed the juno onboarding.
User completed onboarding.
extra_keys:
sequence_position:
type: string
@ -2612,6 +2666,25 @@ metrics:
metadata:
tags:
- Experiments
font_list_json:
type: text
lifetime: ping
description: |
A JSON blob representing the installed fonts
send_in_pings:
- font-list
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1858193
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1858193#c2
data_sensitivity:
# Text metrics are _required_ to be web_activity or highly_sensitive, so even though this
# is more like 'technical' (per the Data Review), I'm marking highly sensitive.
- highly_sensitive
notification_emails:
- android-probes@mozilla.com
- tom@mozilla.com
expires: 124
customize_home:
most_visited_sites:
@ -9088,6 +9161,104 @@ awesomebar:
metadata:
tags:
- Search
sponsored_suggestion_clicked:
type: event
description: |
A sponsored suggestion in the awesomebar was clicked.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1871156
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/4914#issuecomment-1874271848
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- lina@mozilla.com
- ttran@mozilla.com
- najiang@mozilla.com
expires: never
extra_keys:
provider: &sponsored_suggestion_provider
description: |
The provider of the sponsored suggestion. Possible values: `amp` (for adMarketplace
suggestions).
type: string
metadata:
tags:
- Search
non_sponsored_suggestion_clicked:
type: event
description: |
A non-sponsored suggestion in the awesomebar was clicked.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1871156
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/4914#issuecomment-1874271848
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- lina@mozilla.com
- ttran@mozilla.com
- najiang@mozilla.com
expires: never
extra_keys:
provider: &non_sponsored_suggestion_provider
description: |
The provider of the non-sponsored suggestion. Possible values: `wikipedia`.
type: string
metadata:
tags:
- Search
sponsored_suggestion_impressed:
type: event
description: |
A sponsored suggestion was visible when the user finished interacting with the awesomebar.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1871156
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/4914#issuecomment-1874271848
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- lina@mozilla.com
- ttran@mozilla.com
- najiang@mozilla.com
expires: never
extra_keys:
provider: *sponsored_suggestion_provider
engagement_abandoned: &awesomebar_engagement_abandoned
description: |
If `true`, the user dismissed the awesomebar without navigating to a destination. If
`false`, the user finished engaging with the awesomebar by navigating to a destination,
like a URL, a search results page, or a suggestion.
type: boolean
metadata:
tags:
- Search
non_sponsored_suggestion_impressed:
type: event
description: |
A non-sponsored suggestion was visible when the user finished interacting with the awesomebar.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1871156
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/4914#issuecomment-1874271848
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
- lina@mozilla.com
- ttran@mozilla.com
- najiang@mozilla.com
expires: never
extra_keys:
provider: *non_sponsored_suggestion_provider
engagement_abandoned: *awesomebar_engagement_abandoned
metadata:
tags:
- Search
android_autofill:
supported:
type: boolean

@ -14,126 +14,8 @@ channels:
includes:
- onboarding.fml.yaml
- pbm.fml.yaml
- messaging-fenix.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"
USER_ESTABLISHED_INSTALL: "number_of_app_launches >=4"
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
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
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
- 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)
- path: ../android-components/components/browser/engine-gecko/geckoview.fml.yaml
channel: release
features:
@ -143,6 +25,15 @@ import:
download-button: true,
open-in-app-button: true
}
- path: ../android-components/components/feature/fxsuggest/fxsuggest.fml.yaml
channel: release
features:
awesomebar-suggestion-provider:
- value:
available-suggestion-types: {
"amp": true,
"wikipedia": true,
}
features:
toolbar:
@ -278,8 +169,8 @@ features:
"feature-setting-value": 0,
"feature-setting-value-pbm": 0,
"feature-setting-detect-only": 0,
"feature-setting-global-rules": 0,
"feature-setting-global-rules-sub-frames": 0,
"feature-setting-global-rules": 1,
"feature-setting-global-rules-sub-frames": 1,
}
defaults:
- channel: developer
@ -289,8 +180,8 @@ features:
"feature-setting-value": 0,
"feature-setting-value-pbm": 1,
"feature-setting-detect-only": 0,
"feature-setting-global-rules": 0,
"feature-setting-global-rules-sub-frames": 0,
"feature-setting-global-rules": 1,
"feature-setting-global-rules-sub-frames": 1,
}
}
- channel: nightly
@ -300,8 +191,8 @@ features:
"feature-setting-value": 0,
"feature-setting-value-pbm": 1,
"feature-setting-detect-only": 0,
"feature-setting-global-rules": 0,
"feature-setting-global-rules-sub-frames": 0,
"feature-setting-global-rules": 1,
"feature-setting-global-rules-sub-frames": 1,
}
}
- channel: beta
@ -311,8 +202,8 @@ features:
"feature-setting-value": 0,
"feature-setting-value-pbm": 1,
"feature-setting-detect-only": 0,
"feature-setting-global-rules": 0,
"feature-setting-global-rules-sub-frames": 0,
"feature-setting-global-rules": 1,
"feature-setting-global-rules-sub-frames": 1,
}
}
unified-search:

@ -2,7 +2,7 @@
features:
juno-onboarding:
description: A feature that shows juno onboarding flow.
description: A feature that shows the onboarding flow.
variables:
conditions:
@ -10,14 +10,16 @@ features:
A collection of out the box conditional expressions to be
used in determining whether a card should show or not.
Each entry maps to a valid JEXL expression.
type: Map<String, String>
type: Map<ConditionName, String>
string-alias: ConditionName
default: {
ALWAYS: "true",
NEVER: "false"
}
cards:
description: Collection of user facing onboarding cards.
type: Map<String, OnboardingCardData>
type: Map<OnboardingCardKey, OnboardingCardData>
string-alias: OnboardingCardKey
default:
default-browser:
card-type: default-browser
@ -109,7 +111,7 @@ objects:
# This should never be defaulted.
default: ""
prerequisites:
type: List<String>
type: List<ConditionName>
description: >
A list of strings corresponding to targeting expressions.
The card will be shown if all expressions are `true` and if
@ -117,7 +119,7 @@ objects:
if the `disqualifiers` table is empty.
default: [ ALWAYS ]
disqualifiers:
type: List<String>
type: List<ConditionName>
description: >
A list of strings corresponding to targeting expressions.
The card will not be shown if any expression is `true`.

@ -77,6 +77,7 @@ cookie-banner-report-site:
- https://github.com/mozilla-mobile/firefox-android/pull/1298#pullrequestreview-1350344223
notification_emails:
- android-probes@mozilla.com
fx-suggest:
description: |
A ping representing a single event occurring with or to a Firefox Suggestion.
@ -91,3 +92,15 @@ fx-suggest:
- lina@mozilla.com
- ttran@mozilla.com
- najiang@mozilla.com
font-list:
description: |
List of fonts installed on the user's device
include_client_id: false
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1858193
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1858193#c2
notification_emails:
- android-probes@mozilla.com
- tom@mozilla.com

@ -55,3 +55,9 @@
# Keep Android Lifecycle methods
# https://bugzilla.mozilla.org/show_bug.cgi?id=1596302
-keep class androidx.lifecycle.** { *; }
-dontwarn java.beans.BeanInfo
-dontwarn java.beans.FeatureDescriptor
-dontwarn java.beans.IntrospectionException
-dontwarn java.beans.Introspector
-dontwarn java.beans.PropertyDescriptor

@ -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>

@ -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)
}
}

@ -67,12 +67,15 @@ def gradlewbuild(gradlewbuild_log):
@pytest.fixture(name="experiment_data")
def fixture_experiment_data(experiment_url):
data = requests.get(experiment_url).json()
for item in data["branches"][0]["features"][0]["value"]["messages"].values():
item["surface"] = "homescreen"
item["style"] = "URGENT"
for count, trigger in enumerate(item["trigger"]):
if "USER_EN_SPEAKER" not in trigger:
del item["trigger"][count]
branches = next(iter(data.get("branches")), None)
features = next(iter(branches.get("features")), None)
if features.get("messages"):
for item in features["value"]["messages"].values():
item["surface"] = "homescreen"
item["style"] = "URGENT"
for count, trigger in enumerate(item["trigger"]):
if "USER_EN_SPEAKER" not in trigger:
del item["trigger"][count]
return [data]

@ -1,5 +1,5 @@
from pathlib import Path
import subprocess
from pathlib import Path
import yaml

@ -24,7 +24,7 @@ class GradlewBuild(object):
test_type = "ui" if smoke else "experimentintegration"
cmd = f"adb shell am instrument -w -e class org.mozilla.fenix.{test_type}.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner"
# if smoke:
# cmd = f"adb shell am instrument -w -e class org.mozilla.fenix.ui.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner"
# cmd = f"adb shell am instrument -w -e class org.mozilla.fenix.ui.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner"
# else:
# cmd = f"adb shell am instrument -w -e class org.mozilla.fenix.experimentintegration.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner"

@ -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])

@ -38,8 +38,10 @@ import org.junit.Assert.assertEquals
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
import org.mozilla.fenix.helpers.Constants.PackageName.PIXEL_LAUNCHER
import org.mozilla.fenix.helpers.Constants.PackageName.YOUTUBE_APP
import org.mozilla.fenix.helpers.Constants.TAG
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.idlingresource.NetworkConnectionIdlingResource
@ -295,12 +297,14 @@ object AppAndSystemHelper {
)
}
fun bringAppToForeground() {
mDevice.pressRecentApps()
mDevice.findObject(UiSelector().resourceId("${TestHelper.packageName}:id/container")).waitForExists(
TestAssetHelper.waitingTime,
)
}
/**
* Brings the app to foregorund by clicking it in the recent apps tray.
* The package name is related to the home screen experience for the Pixel phones produced by Google.
* The recent apps tray on API 30 will always display only 2 apps, even if previously were opened more.
* The index of the most recent opened app will always have index 2, meaning that the previously opened app will have index 1.
*/
fun bringAppToForeground() =
mDevice.findObject(UiSelector().index(2).packageName(PIXEL_LAUNCHER)).clickAndWaitForNewWindow(waitingTimeShort)
fun verifyKeyboardVisibility(isExpectedToBeVisible: Boolean = true) {
mDevice.waitForIdle()

@ -22,6 +22,7 @@ object Constants {
const val PHONE_APP = "com.android.dialer"
const val ANDROID_SETTINGS = "com.android.settings"
const val PRINT_SPOOLER = "com.android.printspooler"
const val PIXEL_LAUNCHER = "com.google.android.apps.nexuslauncher"
}
const val SPEECH_RECOGNITION = "android.speech.action.RECOGNIZE_SPEECH"

@ -146,4 +146,10 @@ object TestAssetHelper {
return TestAsset(url, "", "")
}
fun getGPCTestAsset(server: MockWebServer): TestAsset {
val url = server.url("pages/global_privacy_control.html").toString().toUri()!!
return TestAsset(url, "", "")
}
}

@ -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
}
}

@ -20,7 +20,7 @@ import org.mozilla.fenix.nimbus.JunoOnboarding
import org.mozilla.fenix.nimbus.OnboardingCardData
import org.mozilla.fenix.nimbus.OnboardingCardType
class JunoOnboardingMapperTest {
class OnboardingMapperTest {
@get:Rule
val activityTestRule =

@ -196,32 +196,33 @@
},
"cryptography": {
"hashes": [
"sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67",
"sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311",
"sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8",
"sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13",
"sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143",
"sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f",
"sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829",
"sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd",
"sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397",
"sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac",
"sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d",
"sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a",
"sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839",
"sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e",
"sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6",
"sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9",
"sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860",
"sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca",
"sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91",
"sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d",
"sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714",
"sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb",
"sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"
"sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596",
"sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c",
"sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660",
"sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4",
"sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead",
"sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed",
"sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3",
"sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7",
"sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09",
"sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c",
"sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43",
"sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65",
"sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6",
"sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da",
"sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c",
"sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b",
"sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8",
"sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c",
"sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d",
"sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9",
"sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86",
"sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36",
"sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==41.0.4"
"version": "==41.0.6"
},
"distro": {
"hashes": [

@ -155,6 +155,7 @@ class ComposeHomeScreenTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.goToHomescreen {
}.openCustomizeHomepage {
clickShortcutsButton()
clickJumpBackInButton()
clickRecentBookmarksButton()
clickRecentSearchesButton()
@ -163,7 +164,7 @@ class ComposeHomeScreenTest {
verifyCustomizeHomepageButton(false)
}.openThreeDotMenu {
}.openCustomizeHome {
clickJumpBackInButton()
clickShortcutsButton()
}.goBackToHomeScreen {
verifyCustomizeHomepageButton(true)
}

@ -225,6 +225,10 @@ class ComposeSettingsDeleteBrowsingDataTest {
selectOnlyCookiesCheckBox()
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
clickDialogCancelButton()
verifyCookiesCheckBox(status = true)
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
confirmDeletionAndAssertSnackbar()
exitMenu()
}

@ -7,7 +7,6 @@ package org.mozilla.fenix.ui
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
@ -369,7 +368,6 @@ class CreditCardAutofillTest {
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1512794
@Ignore("Failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1853625")
@Test
fun verifyMultipleCreditCardsCanBeAddedTest() {
val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
@ -576,7 +574,6 @@ class CreditCardAutofillTest {
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1512791
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1854566")
@Test
fun verifyCreditCardRedirectionsToAutofillSectionAfterInterruptionTest() {
homeScreen {

@ -13,7 +13,6 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.IntentReceiverActivity
@ -141,7 +140,6 @@ class CustomTabsTest {
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334761
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807289")
@SmokeTest
@Test
fun verifyDownloadInACustomTabTest() {
@ -311,6 +309,7 @@ class CustomTabsTest {
verifyEnhancedTrackingProtectionSheetStatus(status = "ON", state = true)
}.toggleEnhancedTrackingProtectionFromSheet {
verifyEnhancedTrackingProtectionSheetStatus(status = "OFF", state = false)
}.closeEnhancedTrackingProtectionSheet {
}
openAppFromExternalLink(customTabPage.url.toString())

@ -137,7 +137,7 @@ class DownloadTest {
@Test
fun pauseResumeCancelDownloadTest() {
downloadRobot {
openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "1GB.zip")
openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "3GB.zip")
}
mDevice.openNotification()
notificationShade {
@ -147,7 +147,7 @@ class DownloadTest {
verifySystemNotificationExists("Download paused")
clickDownloadNotificationControlButton("RESUME")
clickDownloadNotificationControlButton("CANCEL")
verifySystemNotificationDoesNotExist("1GB.zip")
verifySystemNotificationDoesNotExist("3GB.zip")
mDevice.pressBack()
}
browserScreen {
@ -260,11 +260,10 @@ class DownloadTest {
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/457112
@Ignore("Failing: https://bugzilla.mozilla.org/show_bug.cgi?id=1840994")
@Test
fun systemNotificationCantBeDismissedWhileInProgressTest() {
downloadRobot {
openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "1GB.zip")
openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "3GB.zip")
}
browserScreen {
}.openNotificationShade {
@ -306,7 +305,7 @@ class DownloadTest {
homeScreen {
}.togglePrivateBrowsingMode()
downloadRobot {
openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "1GB.zip")
openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "3GB.zip")
}
browserScreen {
}.openTabDrawer {
@ -326,7 +325,7 @@ class DownloadTest {
homeScreen {
}.togglePrivateBrowsingMode()
downloadRobot {
openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "1GB.zip")
openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "3GB.zip")
}
browserScreen {
}.openTabDrawer {
@ -367,7 +366,7 @@ class DownloadTest {
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/244125
@Test
fun restartDownloadFromAppNotificationAfterConnectionIsInterruptedTest() {
downloadFile = "1GB.zip"
downloadFile = "3GB.zip"
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {

@ -80,6 +80,8 @@ class EnhancedTrackingProtectionTest {
verifyEnhancedTrackingProtectionLevelSelected("Standard (default)", true)
verifyStandardOptionDescription()
verifyStrictOptionDescription()
verifyGPCTextWithSwitchWidget()
verifyGPCSwitchEnabled(false)
selectTrackingProtectionOption("Custom")
verifyCustomTrackingProtectionSettings()
scrollToElementByText("Standard (default)")

@ -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.")
}
}
}

@ -10,7 +10,6 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
@ -156,6 +155,7 @@ class HomeScreenTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.goToHomescreen {
}.openCustomizeHomepage {
clickShortcutsButton()
clickJumpBackInButton()
clickRecentBookmarksButton()
clickRecentSearchesButton()
@ -164,14 +164,13 @@ class HomeScreenTest {
verifyCustomizeHomepageButton(false)
}.openThreeDotMenu {
}.openCustomizeHome {
clickJumpBackInButton()
clickShortcutsButton()
}.goBackToHomeScreen {
verifyCustomizeHomepageButton(true)
}
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/414970
@Ignore("Failure, more details at: https://bugzilla.mozilla.org/show_bug.cgi?id=1830005")
@SmokeTest
@Test
fun addPrivateBrowsingShortcutFromHomeScreenCFRTest() {

@ -546,18 +546,6 @@ class LoginsTest {
val loginPage = "https://mozilla-mobile.github.io/testapp/v2.0/loginForm.html"
val originWebsite = "mozilla-mobile.github.io"
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
}.openSaveLoginsAndPasswordsOptions {
verifySaveLoginsOptionsView()
verifyAskToSaveRadioButton(true)
verifyNeverSaveSaveRadioButton(false)
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(loginPage.toUri()) {
setPageObjectText(itemWithResId("username"), "mozilla")

@ -81,6 +81,7 @@ class SettingsAddonsTest {
) {
clickInstallAddon(addonName)
}
verifyAddonDownloadOverlay()
verifyAddonPermissionPrompt(addonName)
cancelInstallAddon()
clickInstallAddon(addonName)

@ -219,6 +219,10 @@ class SettingsDeleteBrowsingDataTest {
selectOnlyCookiesCheckBox()
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
clickDialogCancelButton()
verifyCookiesCheckBox(status = true)
clickDeleteBrowsingDataButton()
verifyDeleteBrowsingDataDialog()
confirmDeletionAndAssertSnackbar()
exitMenu()
}

@ -321,7 +321,6 @@ class SettingsSearchTest {
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203312
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1848623")
@Test
fun verifyErrorMessagesForInvalidSearchEngineUrlsTest() {
val customSearchEngine = object {
@ -420,7 +419,6 @@ class SettingsSearchTest {
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
// Goes through the settings and changes the search suggestion toggle, then verifies it changes.
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/23817")
@SmokeTest
@Test
fun verifyShowSearchSuggestionsToggleTest() {

@ -450,7 +450,6 @@ class SettingsSitePermissionsTest {
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1923417
@Ignore("Flaky, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1829889")
@Test
fun verifyDRMControlledContentPermissionSettingsTest() {
navigationToolbar {

@ -15,7 +15,6 @@ import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
@ -98,7 +97,6 @@ class SitePermissionsTest {
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334294
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1815395")
@Test
fun blockAudioVideoPermissionRememberingTheDecisionTest() {
assumeTrue(cameraManager.cameraIdList.isNotEmpty())
@ -122,7 +120,6 @@ class SitePermissionsTest {
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251388
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1815395")
@Test
fun allowAudioVideoPermissionRememberingTheDecisionTest() {
assumeTrue(cameraManager.cameraIdList.isNotEmpty())
@ -164,7 +161,6 @@ class SitePermissionsTest {
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334190
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1815395")
@Test
fun blockMicrophonePermissionRememberingTheDecisionTest() {
assumeTrue(micManager.microphones.isNotEmpty())
@ -187,7 +183,6 @@ class SitePermissionsTest {
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251387
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1815395")
@Test
fun allowMicrophonePermissionRememberingTheDecisionTest() {
assumeTrue(micManager.microphones.isNotEmpty())
@ -228,7 +223,6 @@ class SitePermissionsTest {
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334077
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1815395")
@Test
fun blockCameraPermissionRememberingTheDecisionTest() {
assumeTrue(cameraManager.cameraIdList.isNotEmpty())
@ -251,7 +245,6 @@ class SitePermissionsTest {
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251386
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1815395")
@Test
fun allowCameraPermissionRememberingTheDecisionTest() {
assumeTrue(cameraManager.cameraIdList.isNotEmpty())

@ -13,7 +13,6 @@ import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTouchInput
@ -33,8 +32,10 @@ import org.mozilla.fenix.home.topsites.TopSitesTestTag
*/
class ComposeTopSitesRobot(private val composeTestRule: HomeActivityComposeTestRule) {
fun verifyExistingTopSitesList() =
composeTestRule.onNodeWithTag(TopSitesTestTag.topSites).assertExists()
@OptIn(ExperimentalTestApi::class)
fun verifyExistingTopSitesList() {
composeTestRule.waitUntilExactlyOneExists(hasTestTag(TopSitesTestTag.topSites), timeoutMillis = waitingTime)
}
@OptIn(ExperimentalTestApi::class)
fun verifyExistingTopSiteItem(vararg titles: String) {

@ -6,6 +6,7 @@
package org.mozilla.fenix.ui.robots
import android.util.Log
import androidx.compose.ui.test.ComposeTimeoutException
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertAny
@ -48,6 +49,7 @@ import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.waitForObjects
@ -80,7 +82,13 @@ class SearchRobot {
}
}
fun verifySearchEngineSuggestionResults(rule: ComposeTestRule, vararg searchSuggestions: String, searchTerm: String) {
fun verifySearchEngineSuggestionResults(
rule: ComposeTestRule,
vararg searchSuggestions: String,
searchTerm: String,
shouldEditKeyword: Boolean = false,
numberOfDeletionSteps: Int = 0,
) {
rule.waitForIdle()
for (i in 1..RETRY_COUNT) {
try {
@ -99,6 +107,9 @@ class SearchRobot {
homeScreen {
}.openSearch {
typeSearch(searchTerm)
if (shouldEditKeyword) {
deleteSearchKeywordCharacters(numberOfDeletionSteps = numberOfDeletionSteps)
}
}
}
}
@ -286,6 +297,14 @@ class SearchRobot {
)
}
fun deleteSearchKeywordCharacters(numberOfDeletionSteps: Int) {
for (i in 1..numberOfDeletionSteps) {
mDevice.pressDelete()
Log.i(Constants.TAG, "deleteSearchKeywordCharacters: Pressed keyboard delete button $i times")
mDevice.waitForWindowUpdate(appName, waitingTimeShort)
}
}
class Transition {
private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource

@ -55,6 +55,9 @@ class SettingsSubMenuAddonsManagerRobot {
fun verifyAddonsListIsDisplayed(shouldBeDisplayed: Boolean) =
assertUIObjectExists(addonsList(), exists = shouldBeDisplayed)
fun verifyAddonDownloadOverlay() =
onView(withText(R.string.mozac_add_on_install_progress_caption)).check(matches(isDisplayed()))
fun verifyAddonPermissionPrompt(addonName: String) {
mDevice.waitNotNull(Until.findObject(By.text("Add $addonName?")), waitingTime)

@ -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))

@ -7,6 +7,7 @@ package org.mozilla.fenix.ui.robots
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.Visibility
@ -33,6 +34,8 @@ import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.isChecked
import org.mozilla.fenix.helpers.isEnabled
const val globalPrivacyControlSwitchText = "Tell websites not to share & sell data"
/**
* Implementation of Robot Pattern for the settings Enhanced Tracking Protection sub menu.
*/
@ -66,6 +69,33 @@ class SettingsSubMenuEnhancedTrackingProtectionRobot {
),
).click()
fun scrollToGCPSettings(): ViewInteraction = onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(globalPrivacyControlSwitchText)),
),
)
fun verifyGPCTextWithSwitchWidget() {
onView(
allOf(
withChild(withText(globalPrivacyControlSwitchText)),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
fun verifyGPCSwitchEnabled(enabled: Boolean) {
onView(
allOf(
withChild(withText(globalPrivacyControlSwitchText)),
),
).check(matches(isChecked(enabled)))
}
fun switchGPCToggle() = onView(
allOf(
withChild(withText(globalPrivacyControlSwitchText)),
),
).click()
fun verifyStandardOptionDescription() {
onView(withText(R.string.preference_enhanced_tracking_protection_standard_description_5))
.check(matches(isDisplayed()))

@ -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")))

@ -119,7 +119,7 @@ class SitePermissionsRobot {
fun verifyDRMContentPermissionPrompt(url: String) {
try {
assertUIObjectExists(itemWithText("Allow $url to store data in persistent storage?"))
assertUIObjectExists(itemWithText("Allow $url to play DRM-controlled content?"))
assertItemTextEquals(denyPagePermissionButton, expectedText = "Dont allow")
assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow")
} catch (e: AssertionError) {
@ -127,7 +127,7 @@ class SitePermissionsRobot {
}.openThreeDotMenu {
}.refreshPage {
}.clickRequestDRMControlledContentAccessButton {
assertUIObjectExists(itemWithText("Allow $url to store data in persistent storage?"))
assertUIObjectExists(itemWithText("Allow $url to play DRM-controlled content?"))
assertItemTextEquals(denyPagePermissionButton, expectedText = "Dont allow")
assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow")
}

@ -17,6 +17,16 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- This is needed because the android.permission.CAMERA above automatically
adds a requirements for camera hardware and we don't want add those restrictions -->
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="android.permission.VIBRATE" />
@ -43,7 +53,6 @@
<application
android:name=".FenixApplication"
android:allowBackup="false"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
@ -266,6 +275,14 @@
<data android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/pdf" />
<data android:scheme="content" />
</intent-filter>
<meta-data
android:name="com.android.systemui.action_assist_icon"
android:resource="@mipmap/ic_launcher" />

@ -27,6 +27,8 @@ class AppRequestInterceptor(
this.navController = WeakReference(navController)
}
override fun interceptsAppInitiatedRequests() = true
override fun onLoadRequest(
engineSession: EngineSession,
uri: String,
@ -37,17 +39,27 @@ class AppRequestInterceptor(
isDirectNavigation: Boolean,
isSubframeRequest: Boolean,
): RequestInterceptor.InterceptionResponse? {
return context.components.services.appLinksInterceptor
.onLoadRequest(
engineSession,
uri,
lastUri,
hasUserGesture,
isSameDomain,
isRedirect,
isDirectNavigation,
isSubframeRequest,
)
val services = context.components.services
return services.urlRequestInterceptor.onLoadRequest(
engineSession,
uri,
lastUri,
hasUserGesture,
isSameDomain,
isRedirect,
isDirectNavigation,
isSubframeRequest,
) ?: services.appLinksInterceptor.onLoadRequest(
engineSession,
uri,
lastUri,
hasUserGesture,
isSameDomain,
isRedirect,
isDirectNavigation,
isSubframeRequest,
)
}
override fun onErrorRequest(

@ -78,4 +78,14 @@ object FeatureFlags {
* Enable Meta attribution.
*/
const val metaAttributionEnabled = true
/**
* Enable Toolbar Redesign components and behaviors ready for Nightly.
*/
val completeToolbarRedesignEnabled = Config.channel.isNightlyOrDebug
/**
* Enable Toolbar Redesign partial components and behaviors.
*/
val incompleteToolbarRedesignEnabled = Config.channel.isDebug
}

@ -207,6 +207,9 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
enableEventTimestamps = FxNimbus.features.glean.value().enableEventTimestamps,
)
// Set the metric configuration from Nimbus.
Glean.setMetricsEnabledConfig(FxNimbus.features.glean.value().metricsEnabled)
Glean.initialize(
applicationContext = this,
configuration = configuration.setCustomEndpointIfAvailable(customEndpoint),
@ -214,9 +217,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
buildInfo = GleanBuildInfo.buildInfo,
)
// Set the metric configuration from Nimbus.
Glean.setMetricsEnabledConfig(FxNimbus.features.glean.value().metricsEnabled)
// We avoid blocking the main thread on startup by setting startup metrics on the background thread.
val store = components.core.store
GlobalScope.launch(Dispatchers.IO) {

@ -46,6 +46,7 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.state.action.ContentAction
@ -98,7 +99,11 @@ import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.components.metrics.GrowthDataWorker
import org.mozilla.fenix.components.metrics.fonts.FontEnumerationWorker
import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
import org.mozilla.fenix.databinding.ActivityHomeBinding
import org.mozilla.fenix.debugsettings.data.DefaultDebugSettingsRepository
import org.mozilla.fenix.debugsettings.ui.DebugOverlay
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment
import org.mozilla.fenix.ext.alreadyOnDestination
@ -157,6 +162,8 @@ import org.mozilla.fenix.tabhistory.TabHistoryDialogFragment
import org.mozilla.fenix.tabstray.TabsTrayFragment
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.trackingprotection.TrackingProtectionPanelDialogFragmentDirections
import org.mozilla.fenix.utils.Settings
@ -277,6 +284,36 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
window.decorView.layoutDirection = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
binding = ActivityHomeBinding.inflate(layoutInflater)
if (Config.channel.isNightlyOrDebug) {
lifecycleScope.launch {
val debugSettingsRepository = DefaultDebugSettingsRepository(
context = this@HomeActivity,
writeScope = this,
)
debugSettingsRepository.debugDrawerEnabled
.distinctUntilChanged()
.collect { enabled ->
with(binding.debugOverlay) {
if (enabled) {
visibility = View.VISIBLE
setContent {
FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) {
DebugOverlay()
}
}
} else {
setContent {}
visibility = View.GONE
}
}
}
}
}
setContentView(binding.root)
ProfilerMarkers.addListenerForOnGlobalLayout(components.core.engine, this, binding.root)
@ -294,14 +331,14 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
it.start()
}
if (settings().shouldShowJunoOnboarding(
if (settings().shouldShowOnboarding(
hasUserBeenOnboarded = components.fenixOnboarding.userHasBeenOnboarded(),
isLauncherIntent = intent.toSafeIntent().isLauncherIntent,
)
) {
// Unless activity is recreated due to config change, navigate to onboarding
if (savedInstanceState == null) {
navHost.navController.navigate(NavGraphDirections.actionGlobalJunoOnboarding())
navHost.navController.navigate(NavGraphDirections.actionGlobalOnboarding())
}
} else {
lifecycleScope.launch(IO) {
@ -521,6 +558,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
GrowthDataWorker.sendActivatedSignalIfNeeded(applicationContext)
FontEnumerationWorker.sendActivatedSignalIfNeeded(applicationContext)
ReEngagementNotificationWorker.setReEngagementNotificationIfNeeded(applicationContext)
MessageNotificationWorker.setMessageNotificationWorker(applicationContext)
}
@ -570,17 +608,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.core.store.state.getNormalOrPrivateTabs(private = false).isNotEmpty()
lifecycleScope.launch(IO) {
components.core.bookmarksStorage.getTree(BookmarkRoot.Root.id, true)?.let {
val desktopRootNode = DesktopFolders(
applicationContext,
showMobileRoot = false,
).withOptionalDesktopFolders(it)
settings().desktopBookmarksSize = desktopRootNode.count()
}
val desktopFolders = DesktopFolders(
applicationContext,
showMobileRoot = false,
)
settings().desktopBookmarksSize = desktopFolders.count()
components.core.bookmarksStorage.getTree(BookmarkRoot.Mobile.id, true)?.let {
settings().mobileBookmarksSize = it.count()
}
settings().mobileBookmarksSize = components.core.bookmarksStorage.countBookmarksInTrees(
listOf(BookmarkRoot.Mobile.id),
).toInt()
}
super.onPause()
@ -626,7 +662,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.core.pocketStoriesService.stopPeriodicSponsoredStoriesRefresh()
privateNotificationObserver?.stop()
components.notificationsDelegate.unBindActivity(this)
stopMediaSession()
if (this !is ExternalAppBrowserActivity) {
stopMediaSession()
}
}
override fun onConfigurationChanged(newConfig: Configuration) {

@ -15,6 +15,7 @@ import mozilla.components.feature.intent.ext.sanitize
import mozilla.components.feature.intent.processing.IntentProcessor
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_CATEGORY
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_PACKAGE
import mozilla.components.support.utils.INTENT_TYPE_PDF
import mozilla.components.support.utils.ext.getApplicationInfoCompat
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity.Companion.PRIVATE_BROWSING_MODE
@ -73,6 +74,12 @@ class IntentReceiverActivity : Activity() {
addReferrerInformation(intent)
if (intent.type == INTENT_TYPE_PDF) {
val referrerIsFenix =
intent.getStringExtra(EXTRA_ACTIVITY_REFERRER_PACKAGE) == this.packageName
Events.openedExtPdf.record(Events.OpenedExtPdfExtra(referrerIsFenix))
}
val processor = getIntentProcessors(private).firstOrNull { it.process(intent) }
val intentProcessorType = components.intentProcessors.getType(processor)

@ -34,6 +34,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.webextension.InstallationMethod
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManager
import mozilla.components.feature.addons.AddonManagerException
@ -335,14 +336,15 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
binding?.let { announceForAccessibility(it.addonProgressOverlay.addOnsOverlayText.text) }
}
val installOperation = provideAddonManger().installAddon(
addon,
url = addon.downloadUrl,
installationMethod = InstallationMethod.MANAGER,
onSuccess = {
runIfFragmentIsAttached {
adapter?.updateAddon(it)
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
}
},
onError = { _, _ ->
onError = { _ ->
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
},
)

@ -23,6 +23,7 @@ import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManager
import mozilla.components.feature.addons.AddonManagerException
import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentInstalledAddOnDetailsBinding
@ -37,8 +38,11 @@ import org.mozilla.fenix.ext.showToolbar
class InstalledAddonDetailsFragment : Fragment() {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal lateinit var addon: Addon
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val binding get() = _binding!!
private var _binding: FragmentInstalledAddOnDetailsBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
@ -49,14 +53,14 @@ class InstalledAddonDetailsFragment : Fragment() {
addon = AddonDetailsFragmentArgs.fromBundle(requireNotNull(arguments)).addon
}
_binding = FragmentInstalledAddOnDetailsBinding.inflate(
inflater,
container,
false,
setBindingAndBindUI(
FragmentInstalledAddOnDetailsBinding.inflate(
inflater,
container,
false,
),
)
bindUI()
return binding.root
}
@ -77,6 +81,12 @@ class InstalledAddonDetailsFragment : Fragment() {
_binding = null
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun setBindingAndBindUI(binding: FragmentInstalledAddOnDetailsBinding) {
_binding = binding
bindUI()
}
private fun bindAddon() {
lifecycleScope.launch(Dispatchers.IO) {
try {
@ -116,6 +126,7 @@ class InstalledAddonDetailsFragment : Fragment() {
bindPermissions()
bindAllowInPrivateBrowsingSwitch()
bindRemoveButton()
bindReportButton()
}
@VisibleForTesting
@ -143,7 +154,7 @@ class InstalledAddonDetailsFragment : Fragment() {
switch.setOnCheckedChangeListener { v, isChecked ->
val addonManager = v.context.components.addonManager
switch.isClickable = false
binding.removeAddOn.isEnabled = false
disableButtons()
if (isChecked) {
enableAddon(
addonManager,
@ -155,7 +166,7 @@ class InstalledAddonDetailsFragment : Fragment() {
privateBrowsingSwitch.isChecked = it.isAllowedInPrivateBrowsing()
switch.setText(R.string.mozac_feature_addons_enabled)
binding.settings.isVisible = shouldSettingsBeVisible()
binding.removeAddOn.isEnabled = true
enableButtons()
context?.let {
showSnackBar(
binding.root,
@ -170,7 +181,7 @@ class InstalledAddonDetailsFragment : Fragment() {
onError = {
runIfFragmentIsAttached {
switch.isClickable = true
binding.removeAddOn.isEnabled = true
enableButtons()
switch.setState(addon.isEnabled())
context?.let {
showSnackBar(
@ -194,7 +205,7 @@ class InstalledAddonDetailsFragment : Fragment() {
switch.isClickable = true
privateBrowsingSwitch.isVisible = it.isEnabled()
switch.setText(R.string.mozac_feature_addons_disabled)
binding.removeAddOn.isEnabled = true
enableButtons()
context?.let {
showSnackBar(
binding.root,
@ -210,7 +221,7 @@ class InstalledAddonDetailsFragment : Fragment() {
runIfFragmentIsAttached {
switch.isClickable = true
privateBrowsingSwitch.isClickable = true
binding.removeAddOn.isEnabled = true
enableButtons()
switch.setState(addon.isEnabled())
context?.let {
showSnackBar(
@ -250,6 +261,23 @@ class InstalledAddonDetailsFragment : Fragment() {
}
}
private fun bindReportButton() {
binding.reportAddOn.setOnClickListener {
val shouldCreatePrivateSession = (activity as HomeActivity).browsingModeManager.mode.isPrivate
it.context.components.useCases.tabsUseCases.selectOrAddTab(
url = "${BuildConfig.AMO_BASE_URL}/android/feedback/addon/${addon.id}/",
private = shouldCreatePrivateSession,
ignoreFragment = true,
)
// Send user to the newly open tab.
Navigation.findNavController(it).navigate(
InstalledAddonDetailsFragmentDirections.actionGlobalBrowser(null),
)
}
}
private fun bindSettings() {
binding.settings.apply {
isVisible = shouldSettingsBeVisible()
@ -304,7 +332,7 @@ class InstalledAddonDetailsFragment : Fragment() {
switch.setOnCheckedChangeListener { v, isChecked ->
val addonManager = v.context.components.addonManager
switch.isClickable = false
binding.removeAddOn.isEnabled = false
disableButtons()
addonManager.setAddonAllowedInPrivateBrowsing(
addon,
isChecked,
@ -312,14 +340,14 @@ class InstalledAddonDetailsFragment : Fragment() {
runIfFragmentIsAttached {
this.addon = it
switch.isClickable = true
binding.removeAddOn.isEnabled = true
enableButtons()
}
},
onError = {
runIfFragmentIsAttached {
switch.isChecked = addon.isAllowedInPrivateBrowsing()
switch.isClickable = true
binding.removeAddOn.isEnabled = true
enableButtons()
}
},
)
@ -373,6 +401,17 @@ class InstalledAddonDetailsFragment : Fragment() {
binding.details.isClickable = clickable
binding.permissions.isClickable = clickable
binding.removeAddOn.isClickable = clickable
binding.reportAddOn.isClickable = clickable
}
private fun enableButtons() {
binding.removeAddOn.isEnabled = true
binding.reportAddOn.isEnabled = true
}
private fun disableButtons() {
binding.removeAddOn.isEnabled = false
binding.reportAddOn.isEnabled = false
}
private fun SwitchMaterial.setState(checked: Boolean) {

@ -150,6 +150,7 @@ import org.mozilla.fenix.ext.secure
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.home.SharedViewModel
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.biometric.BiometricPromptFeature
@ -230,6 +231,7 @@ abstract class BaseBrowserFragment :
internal val sharedViewModel: SharedViewModel by activityViewModels()
private val homeViewModel: HomeScreenViewModel by activityViewModels()
private val bookmarksSharedViewModel: BookmarksSharedViewModel by activityViewModels()
private var currentStartDownloadDialog: StartDownloadDialog? = null
@ -1414,7 +1416,7 @@ abstract class BaseBrowserFragment :
// Save bookmark, then go to edit fragment
try {
val guid = bookmarksStorage.addItem(
BookmarkRoot.Mobile.id,
bookmarksSharedViewModel.selectedFolder?.guid ?: BookmarkRoot.Mobile.id,
url = sessionUrl,
title = sessionTitle,
position = null,

@ -25,6 +25,8 @@ import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.support.ktx.android.view.getRectWithViewLocation
import mozilla.components.support.utils.ext.bottom
import mozilla.components.support.utils.ext.mandatorySystemGestureInsets
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getRectWithScreenLocation
import org.mozilla.fenix.ext.getWindowInsets
@ -260,6 +262,7 @@ class ToolbarGestureHandler(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
tabPreview.isVisible = false
Events.toolbarTabSwipe.record(NoExtras())
}
},
)

@ -207,7 +207,7 @@ class Components(private val context: Context) {
)
}
val fxSuggest by lazyMonitored { FxSuggest(context) }
val fxSuggest by lazyMonitored { FxSuggest(context, analytics.crashReporter) }
}
/**

@ -142,11 +142,13 @@ class Core(
R.color.fx_mobile_layer_color_1,
),
httpsOnlyMode = context.settings().getHttpsOnlyMode(),
globalPrivacyControlEnabled = context.settings().shouldEnableGlobalPrivacyControl,
cookieBannerHandlingMode = context.settings().getCookieBannerHandling(),
cookieBannerHandlingModePrivateBrowsing = context.settings().getCookieBannerHandlingPrivateMode(),
cookieBannerHandlingDetectOnlyMode = context.settings().shouldEnableCookieBannerDetectOnly,
cookieBannerHandlingGlobalRules = context.settings().shouldEnableCookieBannerGlobalRules,
cookieBannerHandlingGlobalRulesSubFrames = context.settings().shouldEnableCookieBannerGlobalRulesSubFrame,
emailTrackerBlockingPrivateBrowsing = true,
)
GeckoEngine(

@ -5,6 +5,7 @@
package org.mozilla.fenix.components
import android.content.Context
import mozilla.components.concept.base.crash.CrashReporting
import mozilla.components.feature.fxsuggest.FxSuggestIngestionScheduler
import mozilla.components.feature.fxsuggest.FxSuggestStorage
import org.mozilla.fenix.perf.lazyMonitored
@ -13,10 +14,12 @@ import org.mozilla.fenix.perf.lazyMonitored
* Component group for Firefox Suggest.
*
* @param context The Android application context.
* @param crashReporter An optional [CrashReporting] instance for reporting unexpected caught
* exceptions.
*/
class FxSuggest(context: Context) {
class FxSuggest(context: Context, crashReporter: CrashReporting? = null) {
val storage by lazyMonitored {
FxSuggestStorage(context)
FxSuggestStorage(context, crashReporter)
}
val ingestionScheduler by lazyMonitored {

@ -11,6 +11,7 @@ import kotlinx.coroutines.launch
import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature
import mozilla.components.feature.app.links.AppLinksInterceptor
import mozilla.components.service.fxa.manager.FxaAccountManager
import org.mozilla.fenix.ext.application
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.settings.SupportUtils
@ -38,4 +39,10 @@ class Services(
launchInApp = { context.settings().shouldOpenLinksInApp() },
)
}
val urlRequestInterceptor by lazyMonitored {
UrlRequestInterceptor(
isDeviceRamAboveThreshold = context.application.isDeviceRamAboveThreshold,
)
}
}

@ -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),
)
}
}

@ -15,6 +15,7 @@ import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import org.mozilla.fenix.browser.StandardSnackbarError
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.shopping.ShoppingState
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
@ -250,5 +251,12 @@ sealed class AppAction : Action {
val productPageUrl: String,
val expanded: Boolean,
) : ShoppingAction()
/**
* [ShoppingAction] used to update the recorded product recommendation impressions set.
*/
data class ProductRecommendationImpression(
val key: ShoppingState.ProductRecommendationImpressionKey,
) : ShoppingAction()
}
}

@ -10,12 +10,28 @@ package org.mozilla.fenix.components.appstate.shopping
* @property shoppingSheetExpanded Boolean indicating if the shopping sheet is expanded and visible.
* @property productCardState Map of product url to [CardState] that contains the state of different
* cards in the shopping sheet.
* @property recordedProductRecommendationImpressions Set of [ProductRecommendationImpressionKey]
* that contains the product recommendation impressions that have been recorded.
*/
data class ShoppingState(
val shoppingSheetExpanded: Boolean? = null,
val productCardState: Map<String, CardState> = emptyMap(),
val recordedProductRecommendationImpressions: Set<ProductRecommendationImpressionKey> = emptySet(),
) {
/**
* Key for a product recommendation impression.
*
* @property tabId The id of the tab that the product and recommendation is displayed in.
* @property productUrl The url of the product.
* @property aid The id of the recommendation.
*/
data class ProductRecommendationImpressionKey(
val tabId: String,
val productUrl: String,
val aid: String,
)
/**
* State for different cards in the shopping sheet for a product.
*

@ -64,6 +64,13 @@ internal object ShoppingStateReducer {
),
)
}
is ShoppingAction.ProductRecommendationImpression -> state.copy(
shoppingState = state.shoppingState.copy(
recordedProductRecommendationImpressions =
state.shoppingState.recordedProductRecommendationImpressions + action.key,
),
)
}
private fun ShoppingState.updateProductCardState(key: String, value: CardState): ShoppingState =

@ -29,13 +29,18 @@ class BookmarksUseCase(
* one with the identical [url] already exists.
*/
@WorkerThread
suspend operator fun invoke(url: String, title: String, position: UInt? = null): Boolean {
suspend operator fun invoke(
url: String,
title: String,
position: UInt? = null,
parentGuid: String? = null,
): Boolean {
return try {
val canAdd = storage.getBookmarksWithUrl(url).firstOrNull { it.url == url } == null
if (canAdd) {
storage.addItem(
BookmarkRoot.Mobile.id,
parentGuid ?: BookmarkRoot.Mobile.id,
url = url,
title = title,
position = position,

@ -283,12 +283,33 @@ internal class ReleaseMetricController(
Component.FEATURE_FXSUGGEST to FxSuggestFacts.Items.AMP_SUGGESTION_CLICKED,
Component.FEATURE_FXSUGGEST to FxSuggestFacts.Items.WIKIPEDIA_SUGGESTION_CLICKED,
-> {
val clickInfo = metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO)
// Record an event for this click in the `events` ping. These events include the `client_id`.
when (clickInfo) {
is FxSuggestInteractionInfo.Amp -> {
Awesomebar.sponsoredSuggestionClicked.record(
Awesomebar.SponsoredSuggestionClickedExtra(
provider = "amp",
),
)
}
is FxSuggestInteractionInfo.Wikipedia -> {
Awesomebar.nonSponsoredSuggestionClicked.record(
Awesomebar.NonSponsoredSuggestionClickedExtra(
provider = "wikipedia",
),
)
}
}
// Submit a separate `fx-suggest` ping for this click. These pings do not include the `client_id`.
FxSuggest.pingType.set("fxsuggest-click")
FxSuggest.isClicked.set(true)
(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)?.let {
FxSuggest.position.set(it)
}
when (val clickInfo = metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO)) {
when (clickInfo) {
is FxSuggestInteractionInfo.Amp -> {
FxSuggest.blockId.set(clickInfo.blockId)
FxSuggest.advertiser.set(clickInfo.advertiser)
@ -307,27 +328,58 @@ internal class ReleaseMetricController(
Component.FEATURE_FXSUGGEST to FxSuggestFacts.Items.AMP_SUGGESTION_IMPRESSED,
Component.FEATURE_FXSUGGEST to FxSuggestFacts.Items.WIKIPEDIA_SUGGESTION_IMPRESSED,
-> {
FxSuggest.pingType.set("fxsuggest-impression")
(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)?.let {
FxSuggest.isClicked.set(it)
}
(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)?.let {
FxSuggest.position.set(it)
}
when (val impressionInfo = metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO)) {
val impressionInfo = metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO)
val engagementAbandoned = metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean
?: false
// Record an event for this impression in the `events` ping. These events include the `client_id`, and
// we record them for engaged and abandoned search sessions.
when (impressionInfo) {
is FxSuggestInteractionInfo.Amp -> {
FxSuggest.blockId.set(impressionInfo.blockId)
FxSuggest.advertiser.set(impressionInfo.advertiser)
FxSuggest.reportingUrl.set(impressionInfo.reportingUrl)
FxSuggest.iabCategory.set(impressionInfo.iabCategory)
FxSuggest.contextId.set(UUID.fromString(impressionInfo.contextId))
Awesomebar.sponsoredSuggestionImpressed.record(
Awesomebar.SponsoredSuggestionImpressedExtra(
provider = "amp",
engagementAbandoned = engagementAbandoned,
),
)
}
is FxSuggestInteractionInfo.Wikipedia -> {
FxSuggest.advertiser.set("wikipedia")
FxSuggest.contextId.set(UUID.fromString(impressionInfo.contextId))
Awesomebar.nonSponsoredSuggestionImpressed.record(
Awesomebar.NonSponsoredSuggestionImpressedExtra(
provider = "wikipedia",
engagementAbandoned = engagementAbandoned,
),
)
}
}
Pings.fxSuggest.submit()
// Submit a separate `fx-suggest` ping for this impression. These pings do not include the `client_id`,
// and we submit them for engaged search sessions only.
if (!engagementAbandoned) {
FxSuggest.pingType.set("fxsuggest-impression")
(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)?.let {
FxSuggest.isClicked.set(it)
}
(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)?.let {
FxSuggest.position.set(it)
}
when (impressionInfo) {
is FxSuggestInteractionInfo.Amp -> {
FxSuggest.blockId.set(impressionInfo.blockId)
FxSuggest.advertiser.set(impressionInfo.advertiser)
FxSuggest.reportingUrl.set(impressionInfo.reportingUrl)
FxSuggest.iabCategory.set(impressionInfo.iabCategory)
FxSuggest.contextId.set(UUID.fromString(impressionInfo.contextId))
}
is FxSuggestInteractionInfo.Wikipedia -> {
FxSuggest.advertiser.set("wikipedia")
FxSuggest.contextId.set(UUID.fromString(impressionInfo.contextId))
}
}
Pings.fxSuggest.submit()
}
Unit
}
Component.FEATURE_PWA to ProgressiveWebAppFacts.Items.HOMESCREEN_ICON_TAP -> {

@ -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"
}
}
}

@ -28,6 +28,7 @@ import org.mozilla.fenix.browser.readermode.ReaderModeController
import org.mozilla.fenix.components.toolbar.interactor.BrowserToolbarInteractor
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.HomeScreenViewModel
@ -221,9 +222,9 @@ class DefaultBrowserToolbarController(
}
override fun handleTranslationsButtonClick() {
navController.navigate(
BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment(),
)
val directions =
BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment()
navController.navigateSafe(R.id.browserFragment, directions)
}
companion object {

@ -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
}

@ -35,6 +35,10 @@ import org.mozilla.fenix.theme.FirefoxTheme
* bounds defined by the width and height.
* @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be used
* if the bounds are a different size from the intrinsic size of the [Painter].
* @param placeholder composable displayed while the image is still loading.
* By default set to a solid color in [DefaultImagePlaceholder].
* @param fallback composable displayed when the image fails loading.
* By default set to a solid color in [DefaultImagePlaceholder].
*/
@Composable
@Suppress("LongParameterList")
@ -46,9 +50,11 @@ fun Image(
contentDescription: String? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
placeholder: @Composable () -> Unit = { DefaultImagePlaceholder(modifier, contentDescription) },
fallback: @Composable () -> Unit = { DefaultImagePlaceholder(modifier, contentDescription) },
) {
if (inComposePreview) {
DefaultImagePlaceholder(modifier = modifier)
placeholder()
} else {
ImageLoader(
url = url,
@ -66,9 +72,9 @@ fun Image(
)
}
WithDefaultPlaceholder(modifier, contentDescription)
WithPlaceholder(placeholder)
WithDefaultFallback(modifier, contentDescription)
WithFallback(fallback)
}
}
}

@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import mozilla.components.support.images.compose.loader.Fallback
@ -19,38 +20,32 @@ import mozilla.components.support.images.compose.loader.Placeholder
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Renders the app default image placeholder while the image is still getting loaded.
* Renders the app image placeholder while the image is still getting loaded.
*
* @param modifier [Modifier] allowing to control among others the dimensions and shape of the image.
* @param contentDescription Text provided to accessibility services to describe what this image represents.
* Defaults to [null] suited for an image used only for decorative purposes and not to be read by
* accessibility services.
* @param placeholder [Composable] composable used during loading.
* By default, set to [DefaultImagePlaceholder] in [org.mozilla.fenix.compose.Image].
*/
@Composable
internal fun ImageLoaderScope.WithDefaultPlaceholder(
modifier: Modifier,
contentDescription: String? = null,
internal fun ImageLoaderScope.WithPlaceholder(
placeholder: @Composable () -> Unit,
) {
Placeholder {
DefaultImagePlaceholder(modifier, contentDescription)
placeholder()
}
}
/**
* Renders the app default image placeholder if loading the image failed.
* Renders the app image placeholder if loading image failed.
*
* @param modifier [Modifier] allowing to control among others the dimensions and shape of the image.
* @param contentDescription Text provided to accessibility services to describe what this image represents.
* Defaults to [null] suited for an image used only for decorative purposes and not to be read by
* accessibility services.
* @param fallback [Painter] composable used if loading failed.
* By default, set to [DefaultImagePlaceholder] in [org.mozilla.fenix.compose.Image].
*/
@Composable
internal fun ImageLoaderScope.WithDefaultFallback(
modifier: Modifier,
contentDescription: String? = null,
internal fun ImageLoaderScope.WithFallback(
fallback: @Composable () -> Unit,
) {
Fallback {
DefaultImagePlaceholder(modifier, contentDescription)
fallback()
}
}
@ -75,7 +70,7 @@ internal fun DefaultImagePlaceholder(
private fun DefaultImagePlaceholderPreview() {
FirefoxTheme {
DefaultImagePlaceholder(
Modifier
modifier = Modifier
.size(200.dp, 100.dp)
.clip(RoundedCornerShape(8.dp)),
)

@ -12,6 +12,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.FloatingActionButtonDefaults
import androidx.compose.material.FloatingActionButtonElevation
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -35,6 +37,8 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param modifier [Modifier] to be applied to the action button.
* @param contentDescription The content description to describe the icon.
* @param label Text to be displayed next to the icon.
* @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in different states.
* This controls the size of the shadow below the FAB.
* @param onClick Invoked when the button is clicked.
*/
@Composable
@ -43,6 +47,7 @@ fun FloatingActionButton(
modifier: Modifier = Modifier,
contentDescription: String? = null,
label: String? = null,
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(defaultElevation = 5.dp),
onClick: () -> Unit,
) {
FloatingActionButton(
@ -50,6 +55,7 @@ fun FloatingActionButton(
modifier = modifier,
backgroundColor = FirefoxTheme.colors.actionPrimary,
contentColor = FirefoxTheme.colors.textActionPrimary,
elevation = elevation,
) {
Row(
modifier = Modifier

@ -19,14 +19,23 @@ import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.button.RadioButton
import org.mozilla.fenix.theme.FirefoxTheme
private val LIST_ITEM_HEIGHT = 56.dp
@ -39,6 +48,7 @@ private val ICON_SIZE = 24.dp
*
* @param label The label in the list item.
* @param modifier [Modifier] to be applied to the layout.
* @param maxLabelLines An optional maximum number of lines for the label text to span.
* @param description An optional description text below the label.
* @param maxDescriptionLines An optional maximum number of lines for the description text to span.
* @param onClick Called when the user clicks on the item.
@ -50,6 +60,7 @@ private val ICON_SIZE = 24.dp
fun TextListItem(
label: String,
modifier: Modifier = Modifier,
maxLabelLines: Int = 1,
description: String? = null,
maxDescriptionLines: Int = 1,
onClick: (() -> Unit)? = null,
@ -59,6 +70,7 @@ fun TextListItem(
) {
ListItem(
label = label,
maxLabelLines = maxLabelLines,
modifier = modifier,
description = description,
maxDescriptionLines = maxDescriptionLines,
@ -69,7 +81,8 @@ fun TextListItem(
onClick = onIconClick,
modifier = Modifier
.padding(end = 16.dp)
.size(ICON_SIZE),
.size(ICON_SIZE)
.clearAndSetSemantics {},
) {
Icon(
painter = iconPainter,
@ -200,12 +213,64 @@ fun IconListItem(
)
}
/**
* List item used to display a label with an optional description text and
* a [RadioButton] at the beginning.
*
* @param label The label in the list item.
* @param selected [Boolean] That indicates whether the [RadioButton] is currently selected.
* @param modifier [Modifier] to be applied to the layout.
* @param maxLabelLines An optional maximum number of lines for the label text to span.
* @param description An optional description text below the label.
* @param maxDescriptionLines An optional maximum number of lines for the description text to span.
* @param onClick Called when the user clicks on the item.
*/
@Composable
fun RadioButtonListItem(
label: String,
selected: Boolean,
modifier: Modifier = Modifier,
maxLabelLines: Int = 1,
description: String? = null,
maxDescriptionLines: Int = 1,
onClick: (() -> Unit),
) {
ListItem(
label = label,
modifier = modifier
.clearAndSetSemantics {
this.selected = selected
role = Role.RadioButton
contentDescription = if (description != null) {
"$label.$description"
} else {
label
}
},
maxLabelLines = maxLabelLines,
description = description,
maxDescriptionLines = maxDescriptionLines,
onClick = onClick,
beforeListAction = {
RadioButton(
selected = selected,
modifier = Modifier
.padding(horizontal = 16.dp)
.size(ICON_SIZE)
.clearAndSetSemantics {},
onClick = onClick,
)
},
)
}
/**
* Base list item used to display a label with an optional description text and
* the flexibility to add custom UI to either end of the item.
*
* @param label The label in the list item.
* @param modifier [Modifier] to be applied to the layout.
* @param maxLabelLines An optional maximum number of lines for the label text to span.
* @param description An optional description text below the label.
* @param maxDescriptionLines An optional maximum number of lines for the description text to span.
* @param onClick Called when the user clicks on the item.
@ -216,6 +281,7 @@ fun IconListItem(
private fun ListItem(
label: String,
modifier: Modifier = Modifier,
maxLabelLines: Int = 1,
description: String? = null,
maxDescriptionLines: Int = 1,
onClick: (() -> Unit)? = null,
@ -242,7 +308,7 @@ private fun ListItem(
text = label,
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.subtitle1,
maxLines = 1,
maxLines = maxLabelLines,
)
description?.let {
@ -358,3 +424,23 @@ private fun FaviconListItemPreview() {
}
}
}
@Composable
@LightDarkPreview
private fun RadioButtonListItemPreview() {
val radioOptions =
listOf("Radio button first item", "Radio button second item", "Radio button third item")
val (selectedOption, onOptionSelected) = remember { mutableStateOf(radioOptions[1]) }
FirefoxTheme {
Column(Modifier.background(FirefoxTheme.colors.layer1)) {
radioOptions.forEach { text ->
RadioButtonListItem(
label = text,
description = "$text description",
onClick = { onOptionSelected(text) },
selected = (text == selectedOption),
)
}
}
}
}

@ -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()
}
}
}

@ -161,7 +161,9 @@ class WebExtensionPromptFeature(
return
}
is WebExtensionInstallException.Unknown -> {
is WebExtensionInstallException.UnsupportedAddonType,
is WebExtensionInstallException.Unknown,
-> {
// Making sure we don't have a
// Title = Failed to install
// Message = Failed to install $addonName

@ -76,6 +76,18 @@ class HomeMenuView(
ThemeManager.resolveAttribute(R.attr.textPrimary, context),
),
)
menuButton.get()?.register(
object : mozilla.components.concept.menu.MenuButton.Observer {
override fun onShow() {
// MenuButton used in [HomeMenuView] doesn't emit toolbar facts.
// A wrapper is responsible for that, but we are using the button
// directly, hence recording the event directly.
// Should investigate further: https://bugzilla.mozilla.org/show_bug.cgi?id=1868207
Events.toolbarMenuVisible.record(NoExtras())
}
},
)
}
/**

@ -55,6 +55,7 @@ internal fun normalModeAdapterItems(
}
if (settings.showTopSitesFeature && topSites.isNotEmpty()) {
shouldShowCustomizeHome = true
if (settings.enableComposeTopSites) {
items.add(AdapterItem.TopSites)
} else {

@ -139,6 +139,10 @@ class BookmarkFragmentInteractor(
BookmarkNodeType.ITEM -> {
bookmarksController.handleBookmarkTapped(item)
BookmarksManagement.open.record(NoExtras())
MetricsUtils.recordBookmarkMetrics(
MetricsUtils.BookmarkAction.OPEN,
METRIC_SOURCE,
)
}
BookmarkNodeType.FOLDER -> bookmarksController.handleBookmarkExpand(item)
BookmarkNodeType.SEPARATOR -> throw IllegalStateException("Cannot open separators")

@ -36,7 +36,7 @@ class BookmarkItemMenu(
@VisibleForTesting
@SuppressWarnings("LongMethod")
internal suspend fun menuItems(itemType: BookmarkNodeType, itemId: String): List<TextMenuCandidate> {
val hasAtLeastOneChild = !context.bookmarkStorage.getTree(itemId)?.children.isNullOrEmpty()
val hasAtLeastOneChild = !context.bookmarkStorage.getTree(itemId, false)?.children.isNullOrEmpty()
return listOfNotNull(
if (itemType != BookmarkNodeType.SEPARATOR) {

@ -49,6 +49,15 @@ class DesktopFolders(
}
}
/**
* Return the total number of desktop bookmarks in the storage database.
*/
suspend fun count(): Int {
return bookmarksStorage.countBookmarksInTrees(
listOf(BookmarkRoot.Menu.id, BookmarkRoot.Toolbar.id, BookmarkRoot.Unfiled.id),
).toInt()
}
private suspend fun virtualDesktopFolder(): BookmarkNode? {
val rootNode = bookmarksStorage.getTree(BookmarkRoot.Root.id, recursive = false) ?: return null
return rootNode.copy(title = rootTitles[rootNode.title])

@ -35,11 +35,14 @@ object CustomAttributeProvider : JexlAttributeProvider {
* will unlikely to targeted as expected.
*/
fun getCustomTargetingAttributes(context: Context): JSONObject {
val isFirstRun = context.settings().isFirstNimbusRun
val settings = context.settings()
val isFirstRun = settings.isFirstNimbusRun
val isReviewCheckerEnabled = settings.isReviewQualityCheckEnabled
return JSONObject(
mapOf(
// By convention, we should use snake case.
"is_first_run" to isFirstRun,
"is_review_checker_enabled" to isReviewCheckerEnabled,
// This camelCase attribute is a boolean value represented as a string.
// This is left for backwards compatibility.
@ -74,7 +77,8 @@ object CustomAttributeProvider : JexlAttributeProvider {
UTM_TERM to settings.utmTerm,
UTM_CONTENT to settings.utmContent,
"are_notifications_enabled" to NotificationManagerCompat.from(context).areNotificationsEnabledSafe(),
"are_notifications_enabled" to NotificationManagerCompat.from(context)
.areNotificationsEnabledSafe(),
),
)
}

@ -12,10 +12,10 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
import mozilla.components.service.nimbus.messaging.Message
@ -32,21 +32,21 @@ const val CLICKED_MESSAGE_ID = "clickedMessageId"
const val DISMISSED_MESSAGE_ID = "dismissedMessageId"
/**
* Background [Worker] that polls Nimbus for available [Message]s at a given interval.
* Background [CoroutineWorker] that polls Nimbus for available [Message]s at a given interval.
* A [Notification] will be created using the configuration of the next highest priority [Message]
* if it has not already been displayed.
*/
class MessageNotificationWorker(
context: Context,
workerParameters: WorkerParameters,
) : Worker(context, workerParameters) {
) : CoroutineWorker(context, workerParameters) {
@SuppressWarnings("ReturnCount")
override fun doWork(): Result {
override suspend fun doWork(): Result {
val context = applicationContext
val messagingStorage = context.components.analytics.messagingStorage
val messages = runBlockingIncrement { messagingStorage.getMessages() }
val messages = messagingStorage.getMessages()
val nextMessage =
messagingStorage.getNextMessage(FenixMessageSurfaceId.NOTIFICATION, messages)
?: return Result.success()
@ -67,7 +67,7 @@ class MessageNotificationWorker(
currentBootUniqueIdentifier,
)
runBlockingIncrement { nimbusMessagingController.onMessageDisplayed(updatedMessage) }
nimbusMessagingController.onMessageDisplayed(updatedMessage)
context.components.notificationsDelegate.notify(
MESSAGE_TAG,
@ -137,7 +137,7 @@ class MessageNotificationWorker(
private const val MESSAGE_WORK_NAME = "org.mozilla.fenix.message.work"
/**
* Initialize the [Worker] to begin polling Nimbus.
* Initialize the [CoroutineWorker] to begin polling Nimbus.
*/
fun setMessageNotificationWorker(context: Context) {
val messaging = FxNimbusMessaging.features.messaging

@ -33,8 +33,8 @@ import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.view.JunoOnboardingScreen
import org.mozilla.fenix.onboarding.view.OnboardingPageUiData
import org.mozilla.fenix.onboarding.view.OnboardingScreen
import org.mozilla.fenix.onboarding.view.sequencePosition
import org.mozilla.fenix.onboarding.view.telemetrySequenceId
import org.mozilla.fenix.onboarding.view.toPageUiData
@ -43,9 +43,9 @@ import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.gecko.search.SearchWidgetProvider
/**
* Fragment displaying the juno onboarding flow.
* Fragment displaying the onboarding flow.
*/
class JunoOnboardingFragment : Fragment() {
class OnboardingFragment : Fragment() {
private val pagesToDisplay by lazy {
pagesToDisplay(
@ -53,7 +53,7 @@ class JunoOnboardingFragment : Fragment() {
canShowAddWidgetCard(),
)
}
private val telemetryRecorder by lazy { JunoOnboardingTelemetryRecorder() }
private val telemetryRecorder by lazy { OnboardingTelemetryRecorder() }
private val pinAppWidgetReceiver = WidgetPinnedReceiver()
@SuppressLint("SourceLockedOrientationActivity")
@ -98,7 +98,7 @@ class JunoOnboardingFragment : Fragment() {
@Suppress("LongMethod")
private fun ScreenContent() {
val context = LocalContext.current
JunoOnboardingScreen(
OnboardingScreen(
pagesToDisplay = pagesToDisplay,
onMakeFirefoxDefaultClick = {
activity?.openSetDefaultBrowserOption(useCustomTab = true)
@ -127,8 +127,8 @@ class JunoOnboardingFragment : Fragment() {
},
onSignInButtonClick = {
findNavController().nav(
id = R.id.junoOnboardingFragment,
directions = JunoOnboardingFragmentDirections.actionGlobalTurnOnSync(
id = R.id.onboardingFragment,
directions = OnboardingFragmentDirections.actionGlobalTurnOnSync(
entrypoint = FenixFxAEntryPoint.NewUserOnboarding,
),
)
@ -203,8 +203,8 @@ class JunoOnboardingFragment : Fragment() {
private fun onFinish(sequenceId: String, sequencePosition: String) {
requireComponents.fenixOnboarding.finish()
findNavController().nav(
id = R.id.junoOnboardingFragment,
directions = JunoOnboardingFragmentDirections.actionHome(),
id = R.id.onboardingFragment,
directions = OnboardingFragmentDirections.actionHome(),
)
telemetryRecorder.onOnboardingComplete(
sequenceId = sequenceId,
@ -224,8 +224,7 @@ class JunoOnboardingFragment : Fragment() {
showNotificationPage: Boolean,
showAddWidgetPage: Boolean,
): List<OnboardingPageUiData> {
val junoOnboardingFeature = FxNimbus.features.junoOnboarding.value()
val jexlConditions = junoOnboardingFeature.conditions
val jexlConditions = FxNimbus.features.junoOnboarding.value().conditions
val jexlHelper = requireContext().components.analytics.messagingStorage.helper
return FxNimbus.features.junoOnboarding.value().cards.values.toPageUiData(

@ -8,9 +8,9 @@ import org.mozilla.fenix.GleanMetrics.Onboarding
import org.mozilla.fenix.onboarding.view.OnboardingPageUiData
/**
* Abstraction responsible for recording telemetry events for JunoOnboarding.
* Abstraction responsible for recording telemetry events for Onboarding.
*/
class JunoOnboardingTelemetryRecorder {
class OnboardingTelemetryRecorder {
/**
* Records "onboarding_completed" telemetry event.

@ -14,11 +14,11 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import mozilla.components.support.utils.PendingIntentUtils
import org.mozilla.fenix.onboarding.view.JunoOnboardingScreen
import org.mozilla.fenix.onboarding.view.OnboardingScreen
/**
* Receiver required to catch callback from Launcher when prompted
* to add search widget from the Juno Onboarding.
* to add search widget from Onboarding.
*/
class WidgetPinnedReceiver : BroadcastReceiver() {
@ -44,7 +44,7 @@ class WidgetPinnedReceiver : BroadcastReceiver() {
/**
* Object containing boolean that updates behavior of Add Search Widget
* card from [JunoOnboardingScreen].
* card from [OnboardingScreen].
* - True if widget added successfully and app resumed from launcher add widget dialog.
* - False if dialog opened but widget was not added.
*/

@ -43,7 +43,7 @@ import org.mozilla.fenix.onboarding.WidgetPinnedReceiver.WidgetPinnedState
import org.mozilla.fenix.theme.FirefoxTheme
/**
* A screen for displaying juno onboarding.
* A screen for displaying onboarding.
*
* @param pagesToDisplay List of pages to be displayed in onboarding pager ui.
* @param onMakeFirefoxDefaultClick Invoked when positive button on default browser page is clicked.
@ -61,7 +61,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
*/
@Composable
@Suppress("LongParameterList", "LongMethod")
fun JunoOnboardingScreen(
fun OnboardingScreen(
pagesToDisplay: List<OnboardingPageUiData>,
onMakeFirefoxDefaultClick: () -> Unit,
onSkipDefaultClick: () -> Unit,
@ -116,7 +116,7 @@ fun JunoOnboardingScreen(
}
}
JunoOnboardingContent(
OnboardingContent(
pagesToDisplay = pagesToDisplay,
pagerState = pagerState,
onMakeFirefoxDefaultClick = {
@ -162,7 +162,7 @@ fun JunoOnboardingScreen(
@Composable
@Suppress("LongParameterList")
private fun JunoOnboardingContent(
private fun OnboardingContent(
pagesToDisplay: List<OnboardingPageUiData>,
pagerState: PagerState,
onMakeFirefoxDefaultClick: () -> Unit,
@ -241,10 +241,10 @@ private class DisableForwardSwipeNestedScrollConnection(
@LightDarkPreview
@Composable
private fun JunoOnboardingScreenPreview() {
private fun OnboardingScreenPreview() {
val pageCount = defaultPreviewPages().size
FirefoxTheme {
JunoOnboardingContent(
OnboardingContent(
pagesToDisplay = defaultPreviewPages(),
pagerState = rememberPagerState(initialPage = 0) {
pageCount

@ -28,11 +28,9 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.Core
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.crashes.CrashListActivity
import org.mozilla.fenix.ext.application
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.telemetryName
import org.mozilla.fenix.search.awesomebar.AwesomeBarView.Companion.GOOGLE_SEARCH_ENGINE_NAME
import org.mozilla.fenix.search.toolbar.SearchSelectorInteractor
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
import org.mozilla.fenix.settings.SupportUtils
@ -114,12 +112,6 @@ class SearchDialogController(
val searchEngine = fragmentStore.state.searchEngineSource.searchEngine
val isDefaultEngine = searchEngine == fragmentStore.state.defaultEngine
val additionalHeaders = getAdditionalHeaders(searchEngine)
val flags = if (additionalHeaders.isNullOrEmpty()) {
LoadUrlFlags.none()
} else {
LoadUrlFlags.select(LoadUrlFlags.ALLOW_ADDITIONAL_HEADERS)
}
activity.openToBrowserAndLoad(
searchTermOrURL = url,
@ -127,9 +119,7 @@ class SearchDialogController(
from = BrowserDirection.FromSearchDialog,
engine = searchEngine,
forceSearch = !isDefaultEngine,
flags = flags,
requestDesktopMode = fromHomeScreen && activity.settings().openNextTabInDesktopMode,
additionalHeaders = additionalHeaders,
)
if (url.isUrl() || searchEngine == null) {
@ -195,12 +185,6 @@ class SearchDialogController(
clearToolbarFocus()
val searchEngine = fragmentStore.state.searchEngineSource.searchEngine
val additionalHeaders = getAdditionalHeaders(searchEngine)
val flags = if (additionalHeaders.isNullOrEmpty()) {
LoadUrlFlags.none()
} else {
LoadUrlFlags.select(LoadUrlFlags.ALLOW_ADDITIONAL_HEADERS)
}
activity.openToBrowserAndLoad(
searchTermOrURL = searchTerms,
@ -208,8 +192,6 @@ class SearchDialogController(
from = BrowserDirection.FromSearchDialog,
engine = searchEngine,
forceSearch = true,
flags = flags,
additionalHeaders = additionalHeaders,
)
val searchAccessPoint = when (fragmentStore.state.searchAccessPoint) {
@ -344,20 +326,4 @@ class SearchDialogController(
create().withCenterAlignedButtons()
}
}
private fun getAdditionalHeaders(searchEngine: SearchEngine?): Map<String, String>? {
if (searchEngine?.name != GOOGLE_SEARCH_ENGINE_NAME) {
return null
}
val value = if (activity.applicationContext.application.isDeviceRamAboveThreshold) {
"1"
} else {
"0"
}
return mapOf(
"X-Search-Subdivision" to value,
)
}
}

@ -74,6 +74,7 @@ import mozilla.components.ui.autocomplete.InlineAutocompleteEditText
import mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Awesomebar
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.VoiceSearch
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
@ -863,6 +864,8 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
return
}
Events.browserToolbarQrScanTapped.record(NoExtras())
view?.hideKeyboard()
toolbarView.view.clearFocus()

@ -10,14 +10,16 @@ import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import mozilla.components.browser.state.search.RegionState
import mozilla.components.lib.state.ext.observeAsState
import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
import org.mozilla.fenix.ext.showToolbar
@ -39,7 +41,7 @@ class SecretDebugSettingsFragment : Fragment() {
return ComposeView(requireContext()).apply {
setContent {
FirefoxTheme {
DebugInfo()
SecretDebugSettingsScreen()
}
}
}
@ -47,33 +49,41 @@ class SecretDebugSettingsFragment : Fragment() {
}
@Composable
private fun DebugInfo() {
val store = components.core.store
private fun SecretDebugSettingsScreen() {
val regionState: RegionState by components.core.store.observeAsState(
initialValue = RegionState.Default,
map = { it.search.region ?: RegionState.Default },
)
DebugInfo(regionState = regionState)
}
@Composable
private fun DebugInfo(regionState: RegionState) {
Column(
modifier = Modifier
.padding(8.dp),
) {
Text(
text = stringResource(R.string.debug_info_region_home),
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.onBackground,
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline6,
modifier = Modifier.padding(4.dp),
)
Text(
text = store.state.search.region?.home ?: "Unknown",
color = MaterialTheme.colors.onBackground,
text = regionState.home,
color = FirefoxTheme.colors.textPrimary,
modifier = Modifier.padding(4.dp),
)
Text(
text = stringResource(R.string.debug_info_region_current),
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.onBackground,
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline6,
modifier = Modifier.padding(4.dp),
)
Text(
text = store.state.search.region?.current ?: "Unknown",
color = MaterialTheme.colors.onBackground,
text = regionState.current,
color = FirefoxTheme.colors.textPrimary,
modifier = Modifier.padding(4.dp),
)
}

@ -6,15 +6,19 @@ package org.mozilla.fenix.settings
import android.os.Bundle
import androidx.core.content.edit
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.debugsettings.data.DefaultDebugSettingsRepository
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
@ -28,6 +32,11 @@ class SecretSettingsFragment : PreferenceFragmentCompat() {
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val debugSettingsRepository = DefaultDebugSettingsRepository(
context = requireContext(),
writeScope = lifecycleScope,
)
setPreferencesFromResource(R.xml.secret_settings_preferences, rootKey)
requirePreference<SwitchPreference>(R.string.pref_key_allow_third_party_root_certs).apply {
@ -48,6 +57,12 @@ class SecretSettingsFragment : PreferenceFragmentCompat() {
onPreferenceChangeListener = SharedPreferenceUpdater()
}
requirePreference<SwitchPreference>(R.string.pref_key_toolbar_use_redesign_incomplete).apply {
isVisible = Config.channel.isDebug
isChecked = context.settings().enableIncompleteToolbarRedesign
onPreferenceChangeListener = SharedPreferenceUpdater()
}
requirePreference<SwitchPreference>(R.string.pref_key_enable_tabs_tray_to_compose).apply {
isVisible = true
isChecked = context.settings().enableTabsTrayToCompose
@ -86,6 +101,25 @@ class SecretSettingsFragment : PreferenceFragmentCompat() {
}
}
requirePreference<SwitchPreference>(R.string.pref_key_should_enable_felt_privacy).apply {
isVisible = true
isChecked = context.settings().feltPrivateBrowsingEnabled
onPreferenceChangeListener = SharedPreferenceUpdater()
}
lifecycleScope.launch {
// During initial development, this will only be available in Nightly or Debug builds.
requirePreference<SwitchPreference>(R.string.pref_key_enable_debug_drawer).apply {
isVisible = Config.channel.isNightlyOrDebug
isChecked = debugSettingsRepository.debugDrawerEnabled.first()
onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, newValue ->
debugSettingsRepository.setDebugDrawerEnabled(enabled = newValue as Boolean)
true
}
}
}
// for performance reasons, this is only available in Nightly or Debug builds
requirePreference<EditTextPreference>(R.string.pref_key_custom_glean_server_url).apply {
isVisible = Config.channel.isNightlyOrDebug && BuildConfig.GLEAN_CUSTOM_URL.isNullOrEmpty()

@ -36,6 +36,7 @@ import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
import mozilla.components.feature.addons.ui.AddonFilePicker
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.ktx.android.view.showKeyboard
import mozilla.components.ui.widgets.withCenterAlignedButtons
@ -70,6 +71,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
private val args by navArgs<SettingsFragmentArgs>()
private lateinit var accountUiView: AccountUiView
private lateinit var addonFilePicker: AddonFilePicker
private val profilerViewModel: ProfilerViewModel by activityViewModels()
@VisibleForTesting
@ -107,6 +109,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
updateFxAAllowDomesticChinaServerMenu = ::updateFxAAllowDomesticChinaServerMenu,
)
addonFilePicker = AddonFilePicker(requireContext(), requireComponents.addonManager)
addonFilePicker.registerForResults(this)
// It's important to update the account UI state in onCreate since that ensures we'll never
// display an incorrect state in the UI. We take care to not also call it as part of onResume
// if it was just called here (via the 'creatingFragment' flag).
@ -386,6 +391,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
resources.getString(R.string.pref_key_nimbus_experiments) -> {
SettingsFragmentDirections.actionSettingsFragmentToNimbusExperimentsFragment()
}
resources.getString(R.string.pref_key_install_local_addon) -> {
addonFilePicker.launch()
null
}
resources.getString(R.string.pref_key_override_amo_collection) -> {
val context = requireContext()
val dialogView = LayoutInflater.from(context).inflate(R.layout.amo_collection_override_dialog, null)
@ -488,6 +497,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
(requireContext().components.core.engine.profiler?.isProfilerActive() != null)
}
setupCookieBannerPreference()
setupInstallAddonFromFilePreference(requireContext().settings())
setupAmoCollectionOverridePreference(requireContext().settings())
setupGeckoLogsPreference(requireContext().settings())
setupAllowDomesticChinaFxaServerPreference()
@ -698,6 +708,16 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}
@VisibleForTesting
internal fun setupInstallAddonFromFilePreference(settings: Settings) {
with(requirePreference<Preference>(R.string.pref_key_install_local_addon)) {
// Below Android 10, the OS doesn't seem to recognize
// the "application/x-xpinstall" mime type (for XPI files).
isVisible =
settings.showSecretDebugMenuThisSession && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
}
@VisibleForTesting
internal fun setupHttpsOnlyPreferences() {
val httpsOnlyPreference =

@ -11,6 +11,7 @@ import androidx.preference.CheckBoxPreference
import androidx.preference.DropDownPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.TrackingProtection
import org.mozilla.fenix.HomeActivity
@ -103,6 +104,16 @@ class TrackingProtectionFragment : PreferenceFragmentCompat() {
val preferenceExceptions =
requirePreference<Preference>(R.string.pref_key_tracking_protection_exceptions)
preferenceExceptions.onPreferenceClickListener = exceptionsClickListener
requirePreference<SwitchPreference>(R.string.pref_key_privacy_enable_global_privacy_control).apply {
onPreferenceChangeListener = object : SharedPreferenceUpdater() {
override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
context.components.core.engine.settings.globalPrivacyControlEnabled = newValue as Boolean
context.components.useCases.sessionUseCases.reload.invoke()
return super.onPreferenceChange(preference, newValue)
}
}
}
}
private fun bindTrackingProtectionRadio(

@ -13,6 +13,7 @@ import org.mozilla.fenix.shopping.DefaultShoppingExperienceFeature
import org.mozilla.fenix.shopping.middleware.DefaultNetworkChecker
import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckPreferences
import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckService
import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckTelemetryService
import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckVendorsService
import org.mozilla.fenix.shopping.middleware.GetReviewQualityCheckSumoUrl
import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckNavigationMiddleware

@ -5,8 +5,8 @@
package org.mozilla.fenix.shopping.middleware
/**
* Converts a string to an enum value, ignoring case. If the string does not match any of the
* enum values, the default value is returned.
* Converts a string to an enum value, ignoring case and replacing spaces with underscores.
* If the string does not match any of the enum values, the default value is returned.
*/
inline fun <reified T : Enum<T>> String.asEnumOrDefault(defaultValue: T? = null): T? =
enumValues<T>().firstOrNull { it.name.equals(this, ignoreCase = true) } ?: defaultValue
enumValues<T>().firstOrNull { it.name.equals(this.replace(" ", "_"), ignoreCase = true) } ?: defaultValue

@ -27,6 +27,10 @@ private fun ProductAnalysis.toProductReview(): ProductReviewState =
} else {
ProductReviewState.Error.GenericError
}
} else if (deletedProductReported) {
ProductReviewState.Error.ProductAlreadyReported
} else if (deletedProduct) {
ProductReviewState.Error.ProductNotAvailable
} else if (notEnoughReviews && !needsAnalysis) {
ProductReviewState.Error.NotEnoughReviews
} else {
@ -50,8 +54,8 @@ private fun ProductAnalysis.toProductReview(): ProductReviewState =
private fun Boolean.toAnalysisStatus(): AnalysisStatus =
when (this) {
true -> AnalysisStatus.NEEDS_ANALYSIS
false -> AnalysisStatus.UP_TO_DATE
true -> AnalysisStatus.NeedsAnalysis
false -> AnalysisStatus.UpToDate
}
private fun Highlight.toHighlights(): Map<HighlightType, List<String>>? =
@ -68,7 +72,4 @@ private fun Highlight.highlightsForType(highlightType: HighlightType) =
HighlightType.SHIPPING -> shipping
HighlightType.PACKAGING_AND_APPEARANCE -> appearance
HighlightType.COMPETITIVENESS -> competitiveness
}?.map { it.surroundWithQuotes() }
private fun String.surroundWithQuotes(): String =
"\"$this\""
}

@ -56,79 +56,102 @@ class ReviewQualityCheckNetworkMiddleware(
scope.launch {
when (action) {
FetchProductAnalysis, RetryProductAnalysis -> {
val productAnalysis = reviewQualityCheckService.fetchProductReview()
val productReviewState = productAnalysis.toProductReviewState()
// Here the ProductReviewState should only updated after the analysis status API
// returns a result. This makes sure that the UI doesn't show the reanalyse
// button in case the product analysis is already in progress on the backend.
if (productReviewState.isAnalysisPresentOrNoAnalysisPresent() &&
reviewQualityCheckService.analysisStatus().isPendingOrInProgress()
) {
store.updateProductReviewState(productReviewState, true)
store.dispatch(ReviewQualityCheckAction.RestoreReanalysis)
} else {
store.updateProductReviewState(productReviewState)
}
if (productReviewState is ProductReviewState.AnalysisPresent) {
store.updateRecommendedProductState()
}
store.onFetch()
}
ReviewQualityCheckAction.ReanalyzeProduct,
ReviewQualityCheckAction.AnalyzeProduct,
ReviewQualityCheckAction.RestoreReanalysis,
-> {
val reanalysis = reviewQualityCheckService.reanalyzeProduct()
store.onReanalyze()
}
if (reanalysis == null) {
store.updateProductReviewState(ProductReviewState.Error.GenericError)
return@launch
ReviewQualityCheckAction.ReportProductBackInStock -> {
val status = reviewQualityCheckService.reportBackInStock()
if (status == ReportBackInStockStatusDto.NOT_DELETED) {
store.onFetch()
}
}
val status = pollForAnalysisStatus()
if (status == null ||
status == AnalysisStatusDto.PENDING ||
status == AnalysisStatusDto.IN_PROGRESS
ReviewQualityCheckAction.ToggleProductRecommendation -> {
val state = store.state
if (state is ReviewQualityCheckState.OptedIn &&
state.productReviewState is ProductReviewState.AnalysisPresent &&
state.productRecommendationsPreference == true
) {
// poll failed, reset to previous state
val state = store.state
if (state is ReviewQualityCheckState.OptedIn) {
if (state.productReviewState is ProductReviewState.NoAnalysisPresent) {
store.updateProductReviewState(ProductReviewState.NoAnalysisPresent())
} else if (state.productReviewState is ProductReviewState.AnalysisPresent) {
store.updateProductReviewState(
state.productReviewState.copy(
analysisStatus = AnalysisStatus.NEEDS_ANALYSIS,
),
)
}
}
} else {
// poll succeeded, update state
val productAnalysis = reviewQualityCheckService.fetchProductReview()
val productReviewState = productAnalysis.toProductReviewState()
store.updateProductReviewState(productReviewState)
store.updateRecommendedProductState()
}
}
}
}
}
is ReviewQualityCheckAction.RecommendedProductClick -> {
reviewQualityCheckService.recordRecommendedProductClick(action.productAid)
}
private suspend fun Store<ReviewQualityCheckState, ReviewQualityCheckAction>.onFetch() {
val productAnalysis = reviewQualityCheckService.fetchProductReview()
val productReviewState = productAnalysis.toProductReviewState()
is ReviewQualityCheckAction.RecommendedProductImpression -> {
reviewQualityCheckService.recordRecommendedProductImpression(action.productAid)
// Here the ProductReviewState should only updated after the analysis status API
// returns a result. This makes sure that the UI doesn't show the reanalyse
// button in case the product analysis is already in progress on the backend.
if (productReviewState.isAnalysisPresentOrNoAnalysisPresent() &&
reviewQualityCheckService.analysisStatus()?.status.isPendingOrInProgress()
) {
updateProductReviewState(productReviewState, true)
dispatch(ReviewQualityCheckAction.RestoreReanalysis)
} else {
updateProductReviewState(productReviewState)
}
if (productReviewState is ProductReviewState.AnalysisPresent) {
updateRecommendedProductState()
}
}
private suspend fun Store<ReviewQualityCheckState, ReviewQualityCheckAction>.onReanalyze() {
val reanalysis = reviewQualityCheckService.reanalyzeProduct()
if (reanalysis == null) {
updateProductReviewState(ProductReviewState.Error.GenericError)
return
}
val statusProgress = pollForAnalysisStatus {
dispatch(ReviewQualityCheckAction.UpdateAnalysisProgress(it))
}
if (statusProgress == null ||
statusProgress.status == AnalysisStatusDto.PENDING ||
statusProgress.status == AnalysisStatusDto.IN_PROGRESS
) {
// poll failed, reset to previous state
val state = this.state
if (state is ReviewQualityCheckState.OptedIn) {
if (state.productReviewState is ProductReviewState.NoAnalysisPresent) {
updateProductReviewState(ProductReviewState.NoAnalysisPresent())
} else if (state.productReviewState is ProductReviewState.AnalysisPresent) {
updateProductReviewState(
state.productReviewState.copy(analysisStatus = AnalysisStatus.NeedsAnalysis),
)
}
}
} else {
// poll succeeded, update state
val productAnalysis = reviewQualityCheckService.fetchProductReview()
val productReviewState = productAnalysis.toProductReviewState()
updateProductReviewState(productReviewState)
}
}
private suspend fun pollForAnalysisStatus(): AnalysisStatusDto? =
private suspend fun pollForAnalysisStatus(
onEachSuccessfulPoll: (progress: Double) -> Unit,
): AnalysisStatusProgressDto? =
retry(
predicate = { it.isPendingOrInProgress() },
block = { reviewQualityCheckService.analysisStatus() },
predicate = { it?.status.isPendingOrInProgress() },
block = {
reviewQualityCheckService.analysisStatus()?.also {
onEachSuccessfulPoll(it.progress)
}
},
)
private fun Store<ReviewQualityCheckState, ReviewQualityCheckAction>.updateProductReviewState(

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save