v121
harvey186 2024-01-30 15:56:09 +01:00
parent ecd7c8bc60
commit 8aff189c63
763 changed files with 110184 additions and 7115 deletions

View File

@ -10,6 +10,7 @@ Notable features include:
* `about:config` support
* The ability to *attempt* to install a much longer list of add-ons than Mozilla's Fenix version of Firefox accepts. Currently the browser queries [this AMO collection](https://addons.mozilla.org/en-US/firefox/collections/16201230/What-I-want-on-Fenix/) **Most of them will not work**, because they depend on code that Mozilla is still working on writing in `android-components`, but you may attempt to install them. If you don't see an add-on you want, you can [request it](https://github.com/fork-maintainers/iceraven-browser/issues/new).
* Option to suspend tabs to avoid being killed for memory (https://bugzilla.mozilla.org/show_bug.cgi?id=1807364)
* Option not to display recently visited websites at HomePage
* **No warranties or guarantees of security or updates or even stability**! Note that Iceraven Browser includes some unstable code written by Mozilla, with our own added modifications on top, all shipped with the stable version of GeckoView engine. Hence, the browser may contain bugs introduced upstream. Binaries are currently built automatically by our Github release automation. These binaries are signed with a debug key. When we finally publish this somewhere official like F-droid, we will sign the apks with a proper key suitable for public release. Due to the current way we create the releases and sign them, you may not want to rely on such "alpha" quality software as your primary web browser, as it will have bugs. So, use this browser only if you are comfortable with these limitations/potential risks.
**Note/Disclaimer:** Iceraven Browser could not exist without the hardworking folks at the Mozilla Corporation who work on the Mozilla Android Components and Firefox projects, but it is not an official Mozilla product, and is not provided, endorsed, vetted, approved, or secured by Mozilla.

@ -1 +1 @@
Subproject commit fdc1766a8e9bd9dadd5007a6391271ea83ffb7db
Subproject commit a2934ef2d89153e3f7842dcfe71fa81af9888d68

View File

@ -94,7 +94,7 @@ android {
// Changing the build config can cause files that depend on BuildConfig.java to recompile
// so we only set the git hash in release builds to avoid possible recompilation in debug builds.
//buildConfigField "String", "GIT_HASH", "\"${Config.getGitHash()}\""
buildConfigField "String", "GIT_HASH", "\"${Config.getGitHash()}\""
if (gradle.hasProperty("localProperties.autosignReleaseWithDebugKey")) {
signingConfig signingConfigs.debug
@ -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'
]))

2610
app/github.txt Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width">
<body>
<script type = "text/javascript" >
const gpcValue = navigator.globalPrivacyControl
if (gpcValue) {
document.write('<p>GPC is enabled.</p>');
} else {
document.write('<p>GPC not enabled.</p>');
}
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,103 +1,103 @@
grep -RiIl 'leosearch.ddns.net' | xargs sed -i 's/leosearch.ddns.net/leosearch.ddns.net/g'
grep -RiIl 'www.google.com' | xargs sed -i 's/www.google.com/leosearch.ddns.net/g'
java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt:280: url = "leosearch.ddns.net",
res/values-iw/strings.xml:1884: <string name="search_add_custom_engine_search_string_example" formatted="false">יש להחליף את השאילתה עם ״%s״. לדוגמה:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-nn-rNO/strings.xml:1927: <string name="search_add_custom_engine_search_string_example" formatted="false">Byt ut spørjinga med «%s». Eksempel:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-pt-rBR/strings.xml:1965: <string name="search_add_custom_engine_search_string_example" formatted="false">Substitua a consulta por “%s”. Por exemplo:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-my/strings.xml:1332: <string name="search_add_custom_engine_search_string_example" formatted="false">စုံစမ်းမှုကို “%s” ဖြင့်အစားထိုးပါ။ ဥပမာ။ \n https://leosearch.ddns.net/search?q= %s</string>
res/values-gl/strings.xml:1936: <string name="search_add_custom_engine_search_string_example" formatted="false">Substitúír a consulta por «%s». Exemplo:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-fr/strings.xml:1983: <string name="search_add_custom_engine_search_string_example" formatted="false">Remplacer les termes de la recherche par « %s ». Par exemple :\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-dsb/strings.xml:1956: <string name="search_add_custom_engine_search_string_example" formatted="false">Napšašowanje z „%s“ wuměniś. Pśikład: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-de/strings.xml:1990: <string name="search_add_custom_engine_search_string_example" formatted="false">Anfrage durch „%s“ ersetzen. Beispiel:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-lo/strings.xml:1933: <string name="search_add_custom_engine_search_string_example" formatted="false">ແທນທີ່ຄິວລີດ້ວຍ “%s”. ຕົວຢ່າ: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-sat/strings.xml:1926: <string name="search_add_custom_engine_search_string_example" formatted="false">“%s” ᱥᱟᱞᱟᱜ ᱠᱣᱮᱨᱭ ᱵᱚᱫᱚᱞ ᱢᱮ ᱾ ᱡᱮᱢᱚᱱ:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-tl/strings.xml:1566: <string name="search_add_custom_engine_search_string_example" formatted="false">Palitan ang query ng “%s”. Halimbawa:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-sr/strings.xml:1889: <string name="search_add_custom_engine_search_string_example" formatted="false">Замените упит са “%s”. Пример:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-fi/strings.xml:1976: <string name="search_add_custom_engine_search_string_example" formatted="false">Korvaa kysely käyttäen ”%s”. Esimerkki:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-vec/strings.xml:822: <string name="search_add_custom_engine_search_string_example" formatted="false">Sostituire ƚa ciave de reserca co “%s”. Exempio:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-hr/strings.xml:1951: <string name="search_add_custom_engine_search_string_example" formatted="false">Zamijeni upit s „%s”. Primjer:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-es/strings.xml:1978: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con “%s”. Ejemplo:\n https://leosearch.ddns.net/search?q=%s</string>
res/values-sc/strings.xml:1561: <string name="search_add_custom_engine_search_string_example" formatted="false">Sostitui sa chirca cun «%s». Esempru: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-fur/strings.xml:1955: <string name="search_add_custom_engine_search_string_example" formatted="false">Sostituìs il test de ricercje cun “%s”. Esempli:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-tt/strings.xml:1481: <string name="search_add_custom_engine_search_string_example" formatted="false">Сорауны “%s” юлы белән алыштырыгыз. Мисал өчен:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-gd/strings.xml:1892: <string name="search_add_custom_engine_search_string_example" formatted="false">Cuir “%s” an àite na ceist. Ball-eisimpleir:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-ru/strings.xml:1984: <string name="search_add_custom_engine_search_string_example" formatted="false">Замените строку запроса на «%s». Пример:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-kk/strings.xml:1956: <string name="search_add_custom_engine_search_string_example" formatted="false">Сұранымды &quot;%s&quot; жолымен алмастырыңыз. Мысалы:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-te/strings.xml:1403: <string name="search_add_custom_engine_search_string_example" formatted="false">వెతుకుడు పదాన్ని “%s”తో పూరించండి. ఉదాహరణ:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-ug/strings.xml:1855: <string name="search_add_custom_engine_search_string_example" formatted="false">سۈرۈشتۈرۈشنى «%s» غا ئالماشتۇرىدۇ. مەسىلەن:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-ml/strings.xml:1316: <string name="search_add_custom_engine_search_string_example" formatted="false">അന്വേഷണ വാചകത്തിന് പകരം “%s” എന്നത് ഉപയോഗിക്കുക. ഉദാഹരണം: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-hi-rIN/strings.xml:1383: <string formatted="false" name="search_add_custom_engine_search_string_example">“%s” से प्रश्न बदले। उदाहरण:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-cak/strings.xml:1787:Achi\'el: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-yo/strings.xml:1694: <string formatted="false" name="search_add_custom_engine_search_string_example">Rọ́pò ìbéérè pẹ̀lú “%s”. Àpẹẹrẹ:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-cy/strings.xml:1950: <string name="search_add_custom_engine_search_string_example" formatted="false">Disodlir ymholiad â “%s”. Enghraifft:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-pt-rPT/strings.xml:1962: <string name="search_add_custom_engine_search_string_example" formatted="false">Substitua a consulta por “%s”. Exemplo: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-en-rGB/strings.xml:1946: <string name="search_add_custom_engine_search_string_example" formatted="false">Replace query with “%s”. Example:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-kaa/strings.xml:1825: <string name="search_add_custom_engine_search_string_example" formatted="false">Sorawdı “%s” menen almastırıń. Mısalı:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-ka/strings.xml:1955: <string name="search_add_custom_engine_search_string_example" formatted="false">მიუთითეთ „%s“ საძიებო ტექსტად. მაგალითი:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-es-rAR/strings.xml:1980: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con &quot;%s&quot;. Ejemplo:\n https://leosearch.ddns.net/search?q=%s</string>
res/values-bg/strings.xml:1453: <string name="search_add_custom_engine_search_string_example" formatted="false">Заменете заявката с „%s“. Пример:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-nb-rNO/strings.xml:1930: <string name="search_add_custom_engine_search_string_example" formatted="false">Bytt ut spørringen med «%s». Eksempel:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-zh-rCN/strings.xml:1995: <string name="search_add_custom_engine_search_string_example" formatted="false">用“%s”替换查询关键字。示例\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-el/strings.xml:1977: <string name="search_add_custom_engine_search_string_example" formatted="false">Αντικαταστήστε τον όρο αναζήτησης με «%s». Παράδειγμα:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values/strings.xml:1933: <string name="search_add_custom_engine_search_string_example" formatted="false">Replace query with “%s”. Example:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-ca/strings.xml:1967: <string name="search_add_custom_engine_search_string_example" formatted="false">Substituïu la consulta per «%s». Per exemple:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-be/strings.xml:1981: <string name="search_add_custom_engine_search_string_example" formatted="false">Змяніць запыт на “%s”. Прыклад:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-eu/strings.xml:1954: <string name="search_add_custom_engine_search_string_example" formatted="false">Ordezkatu galdera-katea &quot;%s&quot; testuarekin. Adibidez:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-cs/strings.xml:1976: <string name="search_add_custom_engine_search_string_example" formatted="false">Dotaz nahraďte „%s“. Příklad: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-lij/strings.xml:926: <string name="search_add_custom_engine_search_string_example" formatted="false">Cangia a ciave de riçerca con “%s”. Ezenpio:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-en-rCA/strings.xml:1949: <string name="search_add_custom_engine_search_string_example" formatted="false">Replace query with “%s”. Example:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-et/strings.xml:1576: <string name="search_add_custom_engine_search_string_example" formatted="false">Päringu asendamiseks kasuta “%s”. Näiteks \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-es-rCL/strings.xml:1952: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con “%s”. Ejemplo:\n https://leosearch.ddns.net/search?q=%s</string>
res/values-ko/strings.xml:2008: <string name="search_add_custom_engine_search_string_example" formatted="false">쿼리를 “%s”로 대체합니다. 예:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-pa-rIN/strings.xml:1978: <string name="search_add_custom_engine_search_string_example" formatted="false">“%s” ਨਾਲ ਕਿਊਰੀ ਨੂੰ ਤਬਦੀਲ ਕਰੋ। ਮਿਸਾਲ ਵਜੋਂ:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-eo/strings.xml:1964: <string name="search_add_custom_engine_search_string_example" formatted="false">Anstataŭigi la serĉotan tekston per “%s”. Ekzemple:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-uz/strings.xml:1722: <string name="search_add_custom_engine_search_string_example" formatted="false">Soʻrovni “%s” bilan almashtiring. Masalan:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-gu-rIN/strings.xml:1121: <string name="search_add_custom_engine_search_string_example" formatted="false">ક્વેરીને “%s”થી બદલો. ઉદાહરણ:\n https://leosearch.ddns.net/search?q=%s</string>
res/values-sl/strings.xml:1970: <string name="search_add_custom_engine_search_string_example" formatted="false">Zamenjajte poizvedbo z &quot;%s&quot;. Primer: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-rm/strings.xml:1957: <string name="search_add_custom_engine_search_string_example" formatted="false">Remplazzar il term da tschertga cun «%s». Per exempel: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-hsb/strings.xml:1964: <string name="search_add_custom_engine_search_string_example" formatted="false">Naprašowanje z „%s“ wuměnić. Přikład: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-kab/strings.xml:1919: <string name="search_add_custom_engine_search_string_example" formatted="false">Beddel aḍris n unadi “%s”. Amedya: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-sq/strings.xml:1940: <string name="search_add_custom_engine_search_string_example" formatted="false">Zëvendësoni kërkesën me “%s”. Shembull:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-zh-rTW/strings.xml:1991: <string name="search_add_custom_engine_search_string_example" formatted="false">用「%s」取代查詢關鍵字。例如:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-in/strings.xml:1914: <string name="search_add_custom_engine_search_string_example" formatted="false">Ganti kueir dengan “%s”. Contoh:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-da/strings.xml:1945: <string name="search_add_custom_engine_search_string_example" formatted="false">Erstat forespørgslen med “%s”. Eksempel:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-th/strings.xml:1934: <string name="search_add_custom_engine_search_string_example" formatted="false">แทนที่คำค้นด้วย “%s” ตัวอย่าง:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-kmr/strings.xml:1880: <string name="search_add_custom_engine_search_string_example" formatted="false">Lêpirsînê bi “%s”ê pev biguherîne. Mînak:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-is/strings.xml:1949: <string name="search_add_custom_engine_search_string_example" formatted="false">Skipta út fyrirspurninni með “%s”. Dæmi:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-it/strings.xml:1997: <string name="search_add_custom_engine_search_string_example" formatted="false">Sostituire la chiave di ricerca con “%s”. Esempio:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-hy-rAM/strings.xml:1937: <string name="search_add_custom_engine_search_string_example" formatted="false">Հարցումը փոխարինել “%s”-ով: Օրինակ՝ \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-gn/strings.xml:2000: <string name="search_add_custom_engine_search_string_example" formatted="false">Emoambue porandu “%s” ndive. Techapyrã:https://leosearch.ddns.net/search?q=%s</string>
res/values-vi/strings.xml:1944: <string name="search_add_custom_engine_search_string_example" formatted="false">Thay thế chuỗi truy vấn thành “%s”. Ví dụ:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-ar/strings.xml:1397: <string name="search_add_custom_engine_search_string_example" formatted="false">استبدِل الاستعلام بِ‍ ”%s“. مثال:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-kn/strings.xml:1251: <string formatted="false" name="search_add_custom_engine_search_string_example">ಪ್ರಶ್ನೆಯನ್ನು “%s” ನೊಂದಿಗೆ ಬದಲಾಯಿಸಿ. ಉದಾಹರಣೆ: \n https://leosearch.ddns.net/search?q=%s</string>
res/values-trs/strings.xml:1968: <string name="search_add_custom_engine_search_string_example" formatted="false">Nādūnā sa nana\'\'t ngà “%s”. dàj rû\':\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-am/strings.xml:1920: <string name="search_add_custom_engine_search_string_example" formatted="false">ጥያቄውን በ &quot;%s&quot; ይተኩ። ምሳሌ፡- \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-ia/strings.xml:2007: <string name="search_add_custom_engine_search_string_example" formatted="false">Replaciar le recerca con “%s”. Exemplo:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-nl/strings.xml:1955: <string name="search_add_custom_engine_search_string_example" formatted="false">Zoekvraag vervangen door %s. Bijvoorbeeld: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-oc/strings.xml:1969: <string name="search_add_custom_engine_search_string_example" formatted="false">Remplaçar los tèrmes de la recèrca per « %s ». Per exemple :\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-an/strings.xml:1365: <string formatted="false" name="search_add_custom_engine_search_string_example">Substituyir la consulta con “%s”. Eixemplo:\n https://leosearch.ddns.net/search?q=%s</string>
res/values-mr/strings.xml:1298: <string name="search_add_custom_engine_search_string_example" formatted="false">क्वेरी “%s” ने बदला. उदा: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-lt/strings.xml:1442: <string name="search_add_custom_engine_search_string_example" formatted="false">Vietoje užklausos įrašykite „%s“. Pvz.:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-es-rMX/strings.xml:1877: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con “%s”. Ejemplo:\n https://leosearch.ddns.net/search?q=%s</string>
res/values-sv-rSE/strings.xml:1971: <string name="search_add_custom_engine_search_string_example" formatted="false">Byt ut frågan med “%s”. Exempel:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-su/strings.xml:1964: <string name="search_add_custom_engine_search_string_example" formatted="false">Ganti kueri ku “%s”. Conto:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-ta/strings.xml:1056: <string name="search_add_custom_engine_search_string_example" formatted="false">வினவலை “%s” ஆக மாற்றுக. எ.கா:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-ja/strings.xml:1985: <string name="search_add_custom_engine_search_string_example" formatted="false">クエリーを “%s” に置き換えます。例:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-es-rES/strings.xml:1992: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con “%s”. Ejemplo:\n https://leosearch.ddns.net/search?q=%s</string>
res/values-hu/strings.xml:1954: <string name="search_add_custom_engine_search_string_example" formatted="false">A keresés cseréje erre: „%s”. Példa:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-fy-rNL/strings.xml:1941: <string name="search_add_custom_engine_search_string_example" formatted="false">Sykfraach ferfange troch %s. Bygelyks: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-ga-rIE/strings.xml:816: <string formatted="false" name="search_add_custom_engine_search_string_example">Cuir “%s” in áit an iarratais. Mar shampla:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-uk/strings.xml:1966: <string name="search_add_custom_engine_search_string_example" formatted="false">Змініть запит на “%s”. Зразок:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-skr/strings.xml:1847:مثال:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-sk/strings.xml:1965: <string name="search_add_custom_engine_search_string_example" formatted="false">Nahraďte výraz s „%s“. Príklad:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-fa/strings.xml:1858: <string name="search_add_custom_engine_search_string_example" formatted="false">درخواست را با “%s” جایگزین کنید. مثال: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-ro/strings.xml:1066: <string name="search_add_custom_engine_search_string_example" formatted="false">Înlocuiește interogarea cu „%s”. Exemplu: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-tg/strings.xml:1956: <string name="search_add_custom_engine_search_string_example" formatted="false">Сатри дархостро бо “%s” иваз намоед. Масалан:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-br/strings.xml:1900: <string name="search_add_custom_engine_search_string_example" formatted="false">Amsaviñ ar gerioù klasket gant “%s”. Da skouer: \nhttps://leosearch.ddns.net/search?q= %s</string>
res/values-bn/strings.xml:970: <string formatted="false" name="search_add_custom_engine_search_string_example">&quot;%s&quot; দিয়ে কোয়েরি প্রতিস্থাপন করুন। উদাহরণ: \nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-tr/strings.xml:1947: <string name="search_add_custom_engine_search_string_example" formatted="false">Sorguyu “%s” ile değiştirin. Örnek:\nhttps://leosearch.ddns.net/search?q=%s</string>
res/values-co/strings.xml:1982: <string name="search_add_custom_engine_search_string_example" formatted="false">Rimpiazzà i termi di a ricerca da « %s ». Esempiu :\nhttps://leosearch.ddns.net/search?q=%s</string>
java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt:280: url = "www.google.com",
res/values-iw/strings.xml:1884: <string name="search_add_custom_engine_search_string_example" formatted="false">יש להחליף את השאילתה עם ״%s״. לדוגמה:\nhttps://www.google.com/search?q=%s</string>
res/values-nn-rNO/strings.xml:1927: <string name="search_add_custom_engine_search_string_example" formatted="false">Byt ut spørjinga med «%s». Eksempel:\nhttps://www.google.com/search?q=%s</string>
res/values-pt-rBR/strings.xml:1965: <string name="search_add_custom_engine_search_string_example" formatted="false">Substitua a consulta por “%s”. Por exemplo:\nhttps://www.google.com/search?q=%s</string>
res/values-my/strings.xml:1332: <string name="search_add_custom_engine_search_string_example" formatted="false">စုံစမ်းမှုကို “%s” ဖြင့်အစားထိုးပါ။ ဥပမာ။ \n https://www.google.com/search?q= %s</string>
res/values-gl/strings.xml:1936: <string name="search_add_custom_engine_search_string_example" formatted="false">Substitúír a consulta por «%s». Exemplo:\nhttps://www.google.com/search?q=%s</string>
res/values-fr/strings.xml:1983: <string name="search_add_custom_engine_search_string_example" formatted="false">Remplacer les termes de la recherche par « %s ». Par exemple :\nhttps://www.google.com/search?q=%s</string>
res/values-dsb/strings.xml:1956: <string name="search_add_custom_engine_search_string_example" formatted="false">Napšašowanje z „%s“ wuměniś. Pśikład: \nhttps://www.google.com/search?q=%s</string>
res/values-de/strings.xml:1990: <string name="search_add_custom_engine_search_string_example" formatted="false">Anfrage durch „%s“ ersetzen. Beispiel:\nhttps://www.google.com/search?q=%s</string>
res/values-lo/strings.xml:1933: <string name="search_add_custom_engine_search_string_example" formatted="false">ແທນທີ່ຄິວລີດ້ວຍ “%s”. ຕົວຢ່າ: \nhttps://www.google.com/search?q=%s</string>
res/values-sat/strings.xml:1926: <string name="search_add_custom_engine_search_string_example" formatted="false">“%s” ᱥᱟᱞᱟᱜ ᱠᱣᱮᱨᱭ ᱵᱚᱫᱚᱞ ᱢᱮ ᱾ ᱡᱮᱢᱚᱱ:\nhttps://www.google.com/search?q=%s</string>
res/values-tl/strings.xml:1566: <string name="search_add_custom_engine_search_string_example" formatted="false">Palitan ang query ng “%s”. Halimbawa:\nhttps://www.google.com/search?q=%s</string>
res/values-sr/strings.xml:1889: <string name="search_add_custom_engine_search_string_example" formatted="false">Замените упит са “%s”. Пример:\nhttps://www.google.com/search?q=%s</string>
res/values-fi/strings.xml:1976: <string name="search_add_custom_engine_search_string_example" formatted="false">Korvaa kysely käyttäen ”%s”. Esimerkki:\nhttps://www.google.com/search?q=%s</string>
res/values-vec/strings.xml:822: <string name="search_add_custom_engine_search_string_example" formatted="false">Sostituire ƚa ciave de reserca co “%s”. Exempio:\nhttps://www.google.com/search?q=%s</string>
res/values-hr/strings.xml:1951: <string name="search_add_custom_engine_search_string_example" formatted="false">Zamijeni upit s „%s”. Primjer:\nhttps://www.google.com/search?q=%s</string>
res/values-es/strings.xml:1978: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con “%s”. Ejemplo:\n https://www.google.com/search?q=%s</string>
res/values-sc/strings.xml:1561: <string name="search_add_custom_engine_search_string_example" formatted="false">Sostitui sa chirca cun «%s». Esempru: \nhttps://www.google.com/search?q=%s</string>
res/values-fur/strings.xml:1955: <string name="search_add_custom_engine_search_string_example" formatted="false">Sostituìs il test de ricercje cun “%s”. Esempli:\nhttps://www.google.com/search?q=%s</string>
res/values-tt/strings.xml:1481: <string name="search_add_custom_engine_search_string_example" formatted="false">Сорауны “%s” юлы белән алыштырыгыз. Мисал өчен:\nhttps://www.google.com/search?q=%s</string>
res/values-gd/strings.xml:1892: <string name="search_add_custom_engine_search_string_example" formatted="false">Cuir “%s” an àite na ceist. Ball-eisimpleir:\nhttps://www.google.com/search?q=%s</string>
res/values-ru/strings.xml:1984: <string name="search_add_custom_engine_search_string_example" formatted="false">Замените строку запроса на «%s». Пример:\nhttps://www.google.com/search?q=%s</string>
res/values-kk/strings.xml:1956: <string name="search_add_custom_engine_search_string_example" formatted="false">Сұранымды &quot;%s&quot; жолымен алмастырыңыз. Мысалы:\nhttps://www.google.com/search?q=%s</string>
res/values-te/strings.xml:1403: <string name="search_add_custom_engine_search_string_example" formatted="false">వెతుకుడు పదాన్ని “%s”తో పూరించండి. ఉదాహరణ:\nhttps://www.google.com/search?q=%s</string>
res/values-ug/strings.xml:1855: <string name="search_add_custom_engine_search_string_example" formatted="false">سۈرۈشتۈرۈشنى «%s» غا ئالماشتۇرىدۇ. مەسىلەن:\nhttps://www.google.com/search?q=%s</string>
res/values-ml/strings.xml:1316: <string name="search_add_custom_engine_search_string_example" formatted="false">അന്വേഷണ വാചകത്തിന് പകരം “%s” എന്നത് ഉപയോഗിക്കുക. ഉദാഹരണം: \nhttps://www.google.com/search?q=%s</string>
res/values-hi-rIN/strings.xml:1383: <string formatted="false" name="search_add_custom_engine_search_string_example">“%s” से प्रश्न बदले। उदाहरण:\nhttps://www.google.com/search?q=%s</string>
res/values-cak/strings.xml:1787:Achi\'el: \nhttps://www.google.com/search?q=%s</string>
res/values-yo/strings.xml:1694: <string formatted="false" name="search_add_custom_engine_search_string_example">Rọ́pò ìbéérè pẹ̀lú “%s”. Àpẹẹrẹ:\nhttps://www.google.com/search?q=%s</string>
res/values-cy/strings.xml:1950: <string name="search_add_custom_engine_search_string_example" formatted="false">Disodlir ymholiad â “%s”. Enghraifft:\nhttps://www.google.com/search?q=%s</string>
res/values-pt-rPT/strings.xml:1962: <string name="search_add_custom_engine_search_string_example" formatted="false">Substitua a consulta por “%s”. Exemplo: \nhttps://www.google.com/search?q=%s</string>
res/values-en-rGB/strings.xml:1946: <string name="search_add_custom_engine_search_string_example" formatted="false">Replace query with “%s”. Example:\nhttps://www.google.com/search?q=%s</string>
res/values-kaa/strings.xml:1825: <string name="search_add_custom_engine_search_string_example" formatted="false">Sorawdı “%s” menen almastırıń. Mısalı:\nhttps://www.google.com/search?q=%s</string>
res/values-ka/strings.xml:1955: <string name="search_add_custom_engine_search_string_example" formatted="false">მიუთითეთ „%s“ საძიებო ტექსტად. მაგალითი:\nhttps://www.google.com/search?q=%s</string>
res/values-es-rAR/strings.xml:1980: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con &quot;%s&quot;. Ejemplo:\n https://www.google.com/search?q=%s</string>
res/values-bg/strings.xml:1453: <string name="search_add_custom_engine_search_string_example" formatted="false">Заменете заявката с „%s“. Пример:\nhttps://www.google.com/search?q=%s</string>
res/values-nb-rNO/strings.xml:1930: <string name="search_add_custom_engine_search_string_example" formatted="false">Bytt ut spørringen med «%s». Eksempel:\nhttps://www.google.com/search?q=%s</string>
res/values-zh-rCN/strings.xml:1995: <string name="search_add_custom_engine_search_string_example" formatted="false">用“%s”替换查询关键字。示例\nhttps://www.google.com/search?q=%s</string>
res/values-el/strings.xml:1977: <string name="search_add_custom_engine_search_string_example" formatted="false">Αντικαταστήστε τον όρο αναζήτησης με «%s». Παράδειγμα:\nhttps://www.google.com/search?q=%s</string>
res/values/strings.xml:1933: <string name="search_add_custom_engine_search_string_example" formatted="false">Replace query with “%s”. Example:\nhttps://www.google.com/search?q=%s</string>
res/values-ca/strings.xml:1967: <string name="search_add_custom_engine_search_string_example" formatted="false">Substituïu la consulta per «%s». Per exemple:\nhttps://www.google.com/search?q=%s</string>
res/values-be/strings.xml:1981: <string name="search_add_custom_engine_search_string_example" formatted="false">Змяніць запыт на “%s”. Прыклад:\nhttps://www.google.com/search?q=%s</string>
res/values-eu/strings.xml:1954: <string name="search_add_custom_engine_search_string_example" formatted="false">Ordezkatu galdera-katea &quot;%s&quot; testuarekin. Adibidez:\nhttps://www.google.com/search?q=%s</string>
res/values-cs/strings.xml:1976: <string name="search_add_custom_engine_search_string_example" formatted="false">Dotaz nahraďte „%s“. Příklad: \nhttps://www.google.com/search?q=%s</string>
res/values-lij/strings.xml:926: <string name="search_add_custom_engine_search_string_example" formatted="false">Cangia a ciave de riçerca con “%s”. Ezenpio:\nhttps://www.google.com/search?q=%s</string>
res/values-en-rCA/strings.xml:1949: <string name="search_add_custom_engine_search_string_example" formatted="false">Replace query with “%s”. Example:\nhttps://www.google.com/search?q=%s</string>
res/values-et/strings.xml:1576: <string name="search_add_custom_engine_search_string_example" formatted="false">Päringu asendamiseks kasuta “%s”. Näiteks \nhttps://www.google.com/search?q=%s</string>
res/values-es-rCL/strings.xml:1952: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con “%s”. Ejemplo:\n https://www.google.com/search?q=%s</string>
res/values-ko/strings.xml:2008: <string name="search_add_custom_engine_search_string_example" formatted="false">쿼리를 “%s”로 대체합니다. 예:\nhttps://www.google.com/search?q=%s</string>
res/values-pa-rIN/strings.xml:1978: <string name="search_add_custom_engine_search_string_example" formatted="false">“%s” ਨਾਲ ਕਿਊਰੀ ਨੂੰ ਤਬਦੀਲ ਕਰੋ। ਮਿਸਾਲ ਵਜੋਂ:\nhttps://www.google.com/search?q=%s</string>
res/values-eo/strings.xml:1964: <string name="search_add_custom_engine_search_string_example" formatted="false">Anstataŭigi la serĉotan tekston per “%s”. Ekzemple:\nhttps://www.google.com/search?q=%s</string>
res/values-uz/strings.xml:1722: <string name="search_add_custom_engine_search_string_example" formatted="false">Soʻrovni “%s” bilan almashtiring. Masalan:\nhttps://www.google.com/search?q=%s</string>
res/values-gu-rIN/strings.xml:1121: <string name="search_add_custom_engine_search_string_example" formatted="false">ક્વેરીને “%s”થી બદલો. ઉદાહરણ:\n https://www.google.com/search?q=%s</string>
res/values-sl/strings.xml:1970: <string name="search_add_custom_engine_search_string_example" formatted="false">Zamenjajte poizvedbo z &quot;%s&quot;. Primer: \nhttps://www.google.com/search?q=%s</string>
res/values-rm/strings.xml:1957: <string name="search_add_custom_engine_search_string_example" formatted="false">Remplazzar il term da tschertga cun «%s». Per exempel: \nhttps://www.google.com/search?q=%s</string>
res/values-hsb/strings.xml:1964: <string name="search_add_custom_engine_search_string_example" formatted="false">Naprašowanje z „%s“ wuměnić. Přikład: \nhttps://www.google.com/search?q=%s</string>
res/values-kab/strings.xml:1919: <string name="search_add_custom_engine_search_string_example" formatted="false">Beddel aḍris n unadi “%s”. Amedya: \nhttps://www.google.com/search?q=%s</string>
res/values-sq/strings.xml:1940: <string name="search_add_custom_engine_search_string_example" formatted="false">Zëvendësoni kërkesën me “%s”. Shembull:\nhttps://www.google.com/search?q=%s</string>
res/values-zh-rTW/strings.xml:1991: <string name="search_add_custom_engine_search_string_example" formatted="false">用「%s」取代查詢關鍵字。例如:\nhttps://www.google.com/search?q=%s</string>
res/values-in/strings.xml:1914: <string name="search_add_custom_engine_search_string_example" formatted="false">Ganti kueir dengan “%s”. Contoh:\nhttps://www.google.com/search?q=%s</string>
res/values-da/strings.xml:1945: <string name="search_add_custom_engine_search_string_example" formatted="false">Erstat forespørgslen med “%s”. Eksempel:\nhttps://www.google.com/search?q=%s</string>
res/values-th/strings.xml:1934: <string name="search_add_custom_engine_search_string_example" formatted="false">แทนที่คำค้นด้วย “%s” ตัวอย่าง:\nhttps://www.google.com/search?q=%s</string>
res/values-kmr/strings.xml:1880: <string name="search_add_custom_engine_search_string_example" formatted="false">Lêpirsînê bi “%s”ê pev biguherîne. Mînak:\nhttps://www.google.com/search?q=%s</string>
res/values-is/strings.xml:1949: <string name="search_add_custom_engine_search_string_example" formatted="false">Skipta út fyrirspurninni með “%s”. Dæmi:\nhttps://www.google.com/search?q=%s</string>
res/values-it/strings.xml:1997: <string name="search_add_custom_engine_search_string_example" formatted="false">Sostituire la chiave di ricerca con “%s”. Esempio:\nhttps://www.google.com/search?q=%s</string>
res/values-hy-rAM/strings.xml:1937: <string name="search_add_custom_engine_search_string_example" formatted="false">Հարցումը փոխարինել “%s”-ով: Օրինակ՝ \nhttps://www.google.com/search?q=%s</string>
res/values-gn/strings.xml:2000: <string name="search_add_custom_engine_search_string_example" formatted="false">Emoambue porandu “%s” ndive. Techapyrã:https://www.google.com/search?q=%s</string>
res/values-vi/strings.xml:1944: <string name="search_add_custom_engine_search_string_example" formatted="false">Thay thế chuỗi truy vấn thành “%s”. Ví dụ:\nhttps://www.google.com/search?q=%s</string>
res/values-ar/strings.xml:1397: <string name="search_add_custom_engine_search_string_example" formatted="false">استبدِل الاستعلام بِ‍ ”%s“. مثال:\nhttps://www.google.com/search?q=%s</string>
res/values-kn/strings.xml:1251: <string formatted="false" name="search_add_custom_engine_search_string_example">ಪ್ರಶ್ನೆಯನ್ನು “%s” ನೊಂದಿಗೆ ಬದಲಾಯಿಸಿ. ಉದಾಹರಣೆ: \n https://www.google.com/search?q=%s</string>
res/values-trs/strings.xml:1968: <string name="search_add_custom_engine_search_string_example" formatted="false">Nādūnā sa nana\'\'t ngà “%s”. dàj rû\':\nhttps://www.google.com/search?q=%s</string>
res/values-am/strings.xml:1920: <string name="search_add_custom_engine_search_string_example" formatted="false">ጥያቄውን በ &quot;%s&quot; ይተኩ። ምሳሌ፡- \nhttps://www.google.com/search?q=%s</string>
res/values-ia/strings.xml:2007: <string name="search_add_custom_engine_search_string_example" formatted="false">Replaciar le recerca con “%s”. Exemplo:\nhttps://www.google.com/search?q=%s</string>
res/values-nl/strings.xml:1955: <string name="search_add_custom_engine_search_string_example" formatted="false">Zoekvraag vervangen door %s. Bijvoorbeeld: \nhttps://www.google.com/search?q=%s</string>
res/values-oc/strings.xml:1969: <string name="search_add_custom_engine_search_string_example" formatted="false">Remplaçar los tèrmes de la recèrca per « %s ». Per exemple :\nhttps://www.google.com/search?q=%s</string>
res/values-an/strings.xml:1365: <string formatted="false" name="search_add_custom_engine_search_string_example">Substituyir la consulta con “%s”. Eixemplo:\n https://www.google.com/search?q=%s</string>
res/values-mr/strings.xml:1298: <string name="search_add_custom_engine_search_string_example" formatted="false">क्वेरी “%s” ने बदला. उदा: \nhttps://www.google.com/search?q=%s</string>
res/values-lt/strings.xml:1442: <string name="search_add_custom_engine_search_string_example" formatted="false">Vietoje užklausos įrašykite „%s“. Pvz.:\nhttps://www.google.com/search?q=%s</string>
res/values-es-rMX/strings.xml:1877: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con “%s”. Ejemplo:\n https://www.google.com/search?q=%s</string>
res/values-sv-rSE/strings.xml:1971: <string name="search_add_custom_engine_search_string_example" formatted="false">Byt ut frågan med “%s”. Exempel:\nhttps://www.google.com/search?q=%s</string>
res/values-su/strings.xml:1964: <string name="search_add_custom_engine_search_string_example" formatted="false">Ganti kueri ku “%s”. Conto:\nhttps://www.google.com/search?q=%s</string>
res/values-ta/strings.xml:1056: <string name="search_add_custom_engine_search_string_example" formatted="false">வினவலை “%s” ஆக மாற்றுக. எ.கா:\nhttps://www.google.com/search?q=%s</string>
res/values-ja/strings.xml:1985: <string name="search_add_custom_engine_search_string_example" formatted="false">クエリーを “%s” に置き換えます。例:\nhttps://www.google.com/search?q=%s</string>
res/values-es-rES/strings.xml:1992: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con “%s”. Ejemplo:\n https://www.google.com/search?q=%s</string>
res/values-hu/strings.xml:1954: <string name="search_add_custom_engine_search_string_example" formatted="false">A keresés cseréje erre: „%s”. Példa:\nhttps://www.google.com/search?q=%s</string>
res/values-fy-rNL/strings.xml:1941: <string name="search_add_custom_engine_search_string_example" formatted="false">Sykfraach ferfange troch %s. Bygelyks: \nhttps://www.google.com/search?q=%s</string>
res/values-ga-rIE/strings.xml:816: <string formatted="false" name="search_add_custom_engine_search_string_example">Cuir “%s” in áit an iarratais. Mar shampla:\nhttps://www.google.com/search?q=%s</string>
res/values-uk/strings.xml:1966: <string name="search_add_custom_engine_search_string_example" formatted="false">Змініть запит на “%s”. Зразок:\nhttps://www.google.com/search?q=%s</string>
res/values-skr/strings.xml:1847:مثال:\nhttps://www.google.com/search?q=%s</string>
res/values-sk/strings.xml:1965: <string name="search_add_custom_engine_search_string_example" formatted="false">Nahraďte výraz s „%s“. Príklad:\nhttps://www.google.com/search?q=%s</string>
res/values-fa/strings.xml:1858: <string name="search_add_custom_engine_search_string_example" formatted="false">درخواست را با “%s” جایگزین کنید. مثال: \nhttps://www.google.com/search?q=%s</string>
res/values-ro/strings.xml:1066: <string name="search_add_custom_engine_search_string_example" formatted="false">Înlocuiește interogarea cu „%s”. Exemplu: \nhttps://www.google.com/search?q=%s</string>
res/values-tg/strings.xml:1956: <string name="search_add_custom_engine_search_string_example" formatted="false">Сатри дархостро бо “%s” иваз намоед. Масалан:\nhttps://www.google.com/search?q=%s</string>
res/values-br/strings.xml:1900: <string name="search_add_custom_engine_search_string_example" formatted="false">Amsaviñ ar gerioù klasket gant “%s”. Da skouer: \nhttps://www.google.com/search?q= %s</string>
res/values-bn/strings.xml:970: <string formatted="false" name="search_add_custom_engine_search_string_example">&quot;%s&quot; দিয়ে কোয়েরি প্রতিস্থাপন করুন। উদাহরণ: \nhttps://www.google.com/search?q=%s</string>
res/values-tr/strings.xml:1947: <string name="search_add_custom_engine_search_string_example" formatted="false">Sorguyu “%s” ile değiştirin. Örnek:\nhttps://www.google.com/search?q=%s</string>
res/values-co/strings.xml:1982: <string name="search_add_custom_engine_search_string_example" formatted="false">Rimpiazzà i termi di a ricerca da « %s ». Esempiu :\nhttps://www.google.com/search?q=%s</string>

98
app/src/main/google1.sh Normal file
View File

@ -0,0 +1,98 @@
java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt:280: url = "www.google.com",
google.sh:1:java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt:280: url = "www.google.com",
google.sh:2:res/values-iw/strings.xml:1884: <string name="search_add_custom_engine_search_string_example" formatted="false">יש להחליף את השאילתה עם ״%s״. לדוגמה:\nhttps://www.google.com/search?q=%s</string>
google.sh:3:res/values-nn-rNO/strings.xml:1927: <string name="search_add_custom_engine_search_string_example" formatted="false">Byt ut spørjinga med «%s». Eksempel:\nhttps://www.google.com/search?q=%s</string>
google.sh:4:res/values-pt-rBR/strings.xml:1965: <string name="search_add_custom_engine_search_string_example" formatted="false">Substitua a consulta por “%s”. Por exemplo:\nhttps://www.google.com/search?q=%s</string>
google.sh:5:res/values-my/strings.xml:1332: <string name="search_add_custom_engine_search_string_example" formatted="false">စုံစမ်းမှုကို “%s” ဖြင့်အစားထိုးပါ။ ဥပမာ။ \n https://www.google.com/search?q= %s</string>
google.sh:6:res/values-gl/strings.xml:1936: <string name="search_add_custom_engine_search_string_example" formatted="false">Substitúír a consulta por «%s». Exemplo:\nhttps://www.google.com/search?q=%s</string>
google.sh:7:res/values-fr/strings.xml:1983: <string name="search_add_custom_engine_search_string_example" formatted="false">Remplacer les termes de la recherche par « %s ». Par exemple :\nhttps://www.google.com/search?q=%s</string>
google.sh:8:res/values-dsb/strings.xml:1956: <string name="search_add_custom_engine_search_string_example" formatted="false">Napšašowanje z „%s“ wuměniś. Pśikład: \nhttps://www.google.com/search?q=%s</string>
google.sh:9:res/values-de/strings.xml:1990: <string name="search_add_custom_engine_search_string_example" formatted="false">Anfrage durch „%s“ ersetzen. Beispiel:\nhttps://www.google.com/search?q=%s</string>
google.sh:10:res/values-lo/strings.xml:1933: <string name="search_add_custom_engine_search_string_example" formatted="false">ແທນທີ່ຄິວລີດ້ວຍ “%s”. ຕົວຢ່າ: \nhttps://www.google.com/search?q=%s</string>
google.sh:11:res/values-sat/strings.xml:1926: <string name="search_add_custom_engine_search_string_example" formatted="false">“%s” ᱥᱟᱞᱟᱜ ᱠᱣᱮᱨᱭ ᱵᱚᱫᱚᱞ ᱢᱮ ᱾ ᱡᱮᱢᱚᱱ:\nhttps://www.google.com/search?q=%s</string>
google.sh:12:res/values-tl/strings.xml:1566: <string name="search_add_custom_engine_search_string_example" formatted="false">Palitan ang query ng “%s”. Halimbawa:\nhttps://www.google.com/search?q=%s</string>
google.sh:13:res/values-sr/strings.xml:1889: <string name="search_add_custom_engine_search_string_example" formatted="false">Замените упит са “%s”. Пример:\nhttps://www.google.com/search?q=%s</string>
google.sh:14:res/values-fi/strings.xml:1976: <string name="search_add_custom_engine_search_string_example" formatted="false">Korvaa kysely käyttäen ”%s”. Esimerkki:\nhttps://www.google.com/search?q=%s</string>
google.sh:15:res/values-vec/strings.xml:822: <string name="search_add_custom_engine_search_string_example" formatted="false">Sostituire ƚa ciave de reserca co “%s”. Exempio:\nhttps://www.google.com/search?q=%s</string>
google.sh:16:res/values-hr/strings.xml:1951: <string name="search_add_custom_engine_search_string_example" formatted="false">Zamijeni upit s „%s”. Primjer:\nhttps://www.google.com/search?q=%s</string>
google.sh:17:res/values-es/strings.xml:1978: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con “%s”. Ejemplo:\n https://www.google.com/search?q=%s</string>
google.sh:18:res/values-sc/strings.xml:1561: <string name="search_add_custom_engine_search_string_example" formatted="false">Sostitui sa chirca cun «%s». Esempru: \nhttps://www.google.com/search?q=%s</string>
google.sh:19:res/values-fur/strings.xml:1955: <string name="search_add_custom_engine_search_string_example" formatted="false">Sostituìs il test de ricercje cun “%s”. Esempli:\nhttps://www.google.com/search?q=%s</string>
google.sh:20:res/values-tt/strings.xml:1481: <string name="search_add_custom_engine_search_string_example" formatted="false">Сорауны “%s” юлы белән алыштырыгыз. Мисал өчен:\nhttps://www.google.com/search?q=%s</string>
google.sh:21:res/values-gd/strings.xml:1892: <string name="search_add_custom_engine_search_string_example" formatted="false">Cuir “%s” an àite na ceist. Ball-eisimpleir:\nhttps://www.google.com/search?q=%s</string>
google.sh:22:res/values-ru/strings.xml:1984: <string name="search_add_custom_engine_search_string_example" formatted="false">Замените строку запроса на «%s». Пример:\nhttps://www.google.com/search?q=%s</string>
google.sh:23:res/values-kk/strings.xml:1956: <string name="search_add_custom_engine_search_string_example" formatted="false">Сұранымды &quot;%s&quot; жолымен алмастырыңыз. Мысалы:\nhttps://www.google.com/search?q=%s</string>
google.sh:24:res/values-te/strings.xml:1403: <string name="search_add_custom_engine_search_string_example" formatted="false">వెతుకుడు పదాన్ని “%s”తో పూరించండి. ఉదాహరణ:\nhttps://www.google.com/search?q=%s</string>
google.sh:25:res/values-ug/strings.xml:1855: <string name="search_add_custom_engine_search_string_example" formatted="false">سۈرۈشتۈرۈشنى «%s» غا ئالماشتۇرىدۇ. مەسىلەن:\nhttps://www.google.com/search?q=%s</string>
google.sh:26:res/values-ml/strings.xml:1316: <string name="search_add_custom_engine_search_string_example" formatted="false">അന്വേഷണ വാചകത്തിന് പകരം “%s” എന്നത് ഉപയോഗിക്കുക. ഉദാഹരണം: \nhttps://www.google.com/search?q=%s</string>
google.sh:27:res/values-hi-rIN/strings.xml:1383: <string formatted="false" name="search_add_custom_engine_search_string_example">“%s” से प्रश्न बदले। उदाहरण:\nhttps://www.google.com/search?q=%s</string>
google.sh:28:res/values-cak/strings.xml:1787:Achi\'el: \nhttps://www.google.com/search?q=%s</string>
google.sh:29:res/values-yo/strings.xml:1694: <string formatted="false" name="search_add_custom_engine_search_string_example">Rọ́pò ìbéérè pẹ̀lú “%s”. Àpẹẹrẹ:\nhttps://www.google.com/search?q=%s</string>
google.sh:30:res/values-cy/strings.xml:1950: <string name="search_add_custom_engine_search_string_example" formatted="false">Disodlir ymholiad â “%s”. Enghraifft:\nhttps://www.google.com/search?q=%s</string>
google.sh:31:res/values-pt-rPT/strings.xml:1962: <string name="search_add_custom_engine_search_string_example" formatted="false">Substitua a consulta por “%s”. Exemplo: \nhttps://www.google.com/search?q=%s</string>
google.sh:32:res/values-en-rGB/strings.xml:1946: <string name="search_add_custom_engine_search_string_example" formatted="false">Replace query with “%s”. Example:\nhttps://www.google.com/search?q=%s</string>
google.sh:33:res/values-kaa/strings.xml:1825: <string name="search_add_custom_engine_search_string_example" formatted="false">Sorawdı “%s” menen almastırıń. Mısalı:\nhttps://www.google.com/search?q=%s</string>
google.sh:34:res/values-ka/strings.xml:1955: <string name="search_add_custom_engine_search_string_example" formatted="false">მიუთითეთ „%s“ საძიებო ტექსტად. მაგალითი:\nhttps://www.google.com/search?q=%s</string>
google.sh:35:res/values-es-rAR/strings.xml:1980: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con &quot;%s&quot;. Ejemplo:\n https://www.google.com/search?q=%s</string>
google.sh:36:res/values-bg/strings.xml:1453: <string name="search_add_custom_engine_search_string_example" formatted="false">Заменете заявката с „%s“. Пример:\nhttps://www.google.com/search?q=%s</string>
google.sh:37:res/values-nb-rNO/strings.xml:1930: <string name="search_add_custom_engine_search_string_example" formatted="false">Bytt ut spørringen med «%s». Eksempel:\nhttps://www.google.com/search?q=%s</string>
google.sh:38:res/values-zh-rCN/strings.xml:1995: <string name="search_add_custom_engine_search_string_example" formatted="false">用“%s”替换查询关键字。示例\nhttps://www.google.com/search?q=%s</string>
google.sh:39:res/values-el/strings.xml:1977: <string name="search_add_custom_engine_search_string_example" formatted="false">Αντικαταστήστε τον όρο αναζήτησης με «%s». Παράδειγμα:\nhttps://www.google.com/search?q=%s</string>
google.sh:40:res/values/strings.xml:1933: <string name="search_add_custom_engine_search_string_example" formatted="false">Replace query with “%s”. Example:\nhttps://www.google.com/search?q=%s</string>
google.sh:41:res/values-ca/strings.xml:1967: <string name="search_add_custom_engine_search_string_example" formatted="false">Substituïu la consulta per «%s». Per exemple:\nhttps://www.google.com/search?q=%s</string>
google.sh:42:res/values-be/strings.xml:1981: <string name="search_add_custom_engine_search_string_example" formatted="false">Змяніць запыт на “%s”. Прыклад:\nhttps://www.google.com/search?q=%s</string>
google.sh:43:res/values-eu/strings.xml:1954: <string name="search_add_custom_engine_search_string_example" formatted="false">Ordezkatu galdera-katea &quot;%s&quot; testuarekin. Adibidez:\nhttps://www.google.com/search?q=%s</string>
google.sh:44:res/values-cs/strings.xml:1976: <string name="search_add_custom_engine_search_string_example" formatted="false">Dotaz nahraďte „%s“. Příklad: \nhttps://www.google.com/search?q=%s</string>
google.sh:45:res/values-lij/strings.xml:926: <string name="search_add_custom_engine_search_string_example" formatted="false">Cangia a ciave de riçerca con “%s”. Ezenpio:\nhttps://www.google.com/search?q=%s</string>
google.sh:46:res/values-en-rCA/strings.xml:1949: <string name="search_add_custom_engine_search_string_example" formatted="false">Replace query with “%s”. Example:\nhttps://www.google.com/search?q=%s</string>
google.sh:47:res/values-et/strings.xml:1576: <string name="search_add_custom_engine_search_string_example" formatted="false">Päringu asendamiseks kasuta “%s”. Näiteks \nhttps://www.google.com/search?q=%s</string>
google.sh:48:res/values-es-rCL/strings.xml:1952: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con “%s”. Ejemplo:\n https://www.google.com/search?q=%s</string>
google.sh:49:res/values-ko/strings.xml:2008: <string name="search_add_custom_engine_search_string_example" formatted="false">쿼리를 “%s”로 대체합니다. 예:\nhttps://www.google.com/search?q=%s</string>
google.sh:50:res/values-pa-rIN/strings.xml:1978: <string name="search_add_custom_engine_search_string_example" formatted="false">“%s” ਨਾਲ ਕਿਊਰੀ ਨੂੰ ਤਬਦੀਲ ਕਰੋ। ਮਿਸਾਲ ਵਜੋਂ:\nhttps://www.google.com/search?q=%s</string>
google.sh:51:res/values-eo/strings.xml:1964: <string name="search_add_custom_engine_search_string_example" formatted="false">Anstataŭigi la serĉotan tekston per “%s”. Ekzemple:\nhttps://www.google.com/search?q=%s</string>
google.sh:52:res/values-uz/strings.xml:1722: <string name="search_add_custom_engine_search_string_example" formatted="false">Soʻrovni “%s” bilan almashtiring. Masalan:\nhttps://www.google.com/search?q=%s</string>
google.sh:53:res/values-gu-rIN/strings.xml:1121: <string name="search_add_custom_engine_search_string_example" formatted="false">ક્વેરીને “%s”થી બદલો. ઉદાહરણ:\n https://www.google.com/search?q=%s</string>
google.sh:54:res/values-sl/strings.xml:1970: <string name="search_add_custom_engine_search_string_example" formatted="false">Zamenjajte poizvedbo z &quot;%s&quot;. Primer: \nhttps://www.google.com/search?q=%s</string>
google.sh:55:res/values-rm/strings.xml:1957: <string name="search_add_custom_engine_search_string_example" formatted="false">Remplazzar il term da tschertga cun «%s». Per exempel: \nhttps://www.google.com/search?q=%s</string>
google.sh:56:res/values-hsb/strings.xml:1964: <string name="search_add_custom_engine_search_string_example" formatted="false">Naprašowanje z „%s“ wuměnić. Přikład: \nhttps://www.google.com/search?q=%s</string>
google.sh:57:res/values-kab/strings.xml:1919: <string name="search_add_custom_engine_search_string_example" formatted="false">Beddel aḍris n unadi “%s”. Amedya: \nhttps://www.google.com/search?q=%s</string>
google.sh:58:res/values-sq/strings.xml:1940: <string name="search_add_custom_engine_search_string_example" formatted="false">Zëvendësoni kërkesën me “%s”. Shembull:\nhttps://www.google.com/search?q=%s</string>
google.sh:59:res/values-zh-rTW/strings.xml:1991: <string name="search_add_custom_engine_search_string_example" formatted="false">用「%s」取代查詢關鍵字。例如:\nhttps://www.google.com/search?q=%s</string>
google.sh:60:res/values-in/strings.xml:1914: <string name="search_add_custom_engine_search_string_example" formatted="false">Ganti kueir dengan “%s”. Contoh:\nhttps://www.google.com/search?q=%s</string>
google.sh:61:res/values-da/strings.xml:1945: <string name="search_add_custom_engine_search_string_example" formatted="false">Erstat forespørgslen med “%s”. Eksempel:\nhttps://www.google.com/search?q=%s</string>
google.sh:62:res/values-th/strings.xml:1934: <string name="search_add_custom_engine_search_string_example" formatted="false">แทนที่คำค้นด้วย “%s” ตัวอย่าง:\nhttps://www.google.com/search?q=%s</string>
google.sh:63:res/values-kmr/strings.xml:1880: <string name="search_add_custom_engine_search_string_example" formatted="false">Lêpirsînê bi “%s”ê pev biguherîne. Mînak:\nhttps://www.google.com/search?q=%s</string>
google.sh:64:res/values-is/strings.xml:1949: <string name="search_add_custom_engine_search_string_example" formatted="false">Skipta út fyrirspurninni með “%s”. Dæmi:\nhttps://www.google.com/search?q=%s</string>
google.sh:65:res/values-it/strings.xml:1997: <string name="search_add_custom_engine_search_string_example" formatted="false">Sostituire la chiave di ricerca con “%s”. Esempio:\nhttps://www.google.com/search?q=%s</string>
google.sh:66:res/values-hy-rAM/strings.xml:1937: <string name="search_add_custom_engine_search_string_example" formatted="false">Հարցումը փոխարինել “%s”-ով: Օրինակ՝ \nhttps://www.google.com/search?q=%s</string>
google.sh:67:res/values-gn/strings.xml:2000: <string name="search_add_custom_engine_search_string_example" formatted="false">Emoambue porandu “%s” ndive. Techapyrã:https://www.google.com/search?q=%s</string>
google.sh:68:res/values-vi/strings.xml:1944: <string name="search_add_custom_engine_search_string_example" formatted="false">Thay thế chuỗi truy vấn thành “%s”. Ví dụ:\nhttps://www.google.com/search?q=%s</string>
google.sh:69:res/values-ar/strings.xml:1397: <string name="search_add_custom_engine_search_string_example" formatted="false">استبدِل الاستعلام بِ‍ ”%s“. مثال:\nhttps://www.google.com/search?q=%s</string>
google.sh:70:res/values-kn/strings.xml:1251: <string formatted="false" name="search_add_custom_engine_search_string_example">ಪ್ರಶ್ನೆಯನ್ನು “%s” ನೊಂದಿಗೆ ಬದಲಾಯಿಸಿ. ಉದಾಹರಣೆ: \n https://www.google.com/search?q=%s</string>
google.sh:71:res/values-trs/strings.xml:1968: <string name="search_add_custom_engine_search_string_example" formatted="false">Nādūnā sa nana\'\'t ngà “%s”. dàj rû\':\nhttps://www.google.com/search?q=%s</string>
google.sh:72:res/values-am/strings.xml:1920: <string name="search_add_custom_engine_search_string_example" formatted="false">ጥያቄውን በ &quot;%s&quot; ይተኩ። ምሳሌ፡- \nhttps://www.google.com/search?q=%s</string>
google.sh:73:res/values-ia/strings.xml:2007: <string name="search_add_custom_engine_search_string_example" formatted="false">Replaciar le recerca con “%s”. Exemplo:\nhttps://www.google.com/search?q=%s</string>
google.sh:74:res/values-nl/strings.xml:1955: <string name="search_add_custom_engine_search_string_example" formatted="false">Zoekvraag vervangen door %s. Bijvoorbeeld: \nhttps://www.google.com/search?q=%s</string>
google.sh:75:res/values-oc/strings.xml:1969: <string name="search_add_custom_engine_search_string_example" formatted="false">Remplaçar los tèrmes de la recèrca per « %s ». Per exemple :\nhttps://www.google.com/search?q=%s</string>
google.sh:76:res/values-an/strings.xml:1365: <string formatted="false" name="search_add_custom_engine_search_string_example">Substituyir la consulta con “%s”. Eixemplo:\n https://www.google.com/search?q=%s</string>
google.sh:77:res/values-mr/strings.xml:1298: <string name="search_add_custom_engine_search_string_example" formatted="false">क्वेरी “%s” ने बदला. उदा: \nhttps://www.google.com/search?q=%s</string>
google.sh:78:res/values-lt/strings.xml:1442: <string name="search_add_custom_engine_search_string_example" formatted="false">Vietoje užklausos įrašykite „%s“. Pvz.:\nhttps://www.google.com/search?q=%s</string>
google.sh:79:res/values-es-rMX/strings.xml:1877: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con “%s”. Ejemplo:\n https://www.google.com/search?q=%s</string>
google.sh:80:res/values-sv-rSE/strings.xml:1971: <string name="search_add_custom_engine_search_string_example" formatted="false">Byt ut frågan med “%s”. Exempel:\nhttps://www.google.com/search?q=%s</string>
google.sh:81:res/values-su/strings.xml:1964: <string name="search_add_custom_engine_search_string_example" formatted="false">Ganti kueri ku “%s”. Conto:\nhttps://www.google.com/search?q=%s</string>
google.sh:82:res/values-ta/strings.xml:1056: <string name="search_add_custom_engine_search_string_example" formatted="false">வினவலை “%s” ஆக மாற்றுக. எ.கா:\nhttps://www.google.com/search?q=%s</string>
google.sh:83:res/values-ja/strings.xml:1985: <string name="search_add_custom_engine_search_string_example" formatted="false">クエリーを “%s” に置き換えます。例:\nhttps://www.google.com/search?q=%s</string>
google.sh:84:res/values-es-rES/strings.xml:1992: <string name="search_add_custom_engine_search_string_example" formatted="false">Reemplazar la consulta con “%s”. Ejemplo:\n https://www.google.com/search?q=%s</string>
google.sh:85:res/values-hu/strings.xml:1954: <string name="search_add_custom_engine_search_string_example" formatted="false">A keresés cseréje erre: „%s”. Példa:\nhttps://www.google.com/search?q=%s</string>
google.sh:86:res/values-fy-rNL/strings.xml:1941: <string name="search_add_custom_engine_search_string_example" formatted="false">Sykfraach ferfange troch %s. Bygelyks: \nhttps://www.google.com/search?q=%s</string>
google.sh:87:res/values-ga-rIE/strings.xml:816: <string formatted="false" name="search_add_custom_engine_search_string_example">Cuir “%s” in áit an iarratais. Mar shampla:\nhttps://www.google.com/search?q=%s</string>
google.sh:88:res/values-uk/strings.xml:1966: <string name="search_add_custom_engine_search_string_example" formatted="false">Змініть запит на “%s”. Зразок:\nhttps://www.google.com/search?q=%s</string>
google.sh:89:res/values-skr/strings.xml:1847:مثال:\nhttps://www.google.com/search?q=%s</string>
google.sh:90:res/values-sk/strings.xml:1965: <string name="search_add_custom_engine_search_string_example" formatted="false">Nahraďte výraz s „%s“. Príklad:\nhttps://www.google.com/search?q=%s</string>
google.sh:91:res/values-fa/strings.xml:1858: <string name="search_add_custom_engine_search_string_example" formatted="false">درخواست را با “%s” جایگزین کنید. مثال: \nhttps://www.google.com/search?q=%s</string>
google.sh:92:res/values-ro/strings.xml:1066: <string name="search_add_custom_engine_search_string_example" formatted="false">Înlocuiește interogarea cu „%s”. Exemplu: \nhttps://www.google.com/search?q=%s</string>
google.sh:93:res/values-tg/strings.xml:1956: <string name="search_add_custom_engine_search_string_example" formatted="false">Сатри дархостро бо “%s” иваз намоед. Масалан:\nhttps://www.google.com/search?q=%s</string>
google.sh:94:res/values-br/strings.xml:1900: <string name="search_add_custom_engine_search_string_example" formatted="false">Amsaviñ ar gerioù klasket gant “%s”. Da skouer: \nhttps://www.google.com/search?q= %s</string>
google.sh:95:res/values-bn/strings.xml:970: <string formatted="false" name="search_add_custom_engine_search_string_example">&quot;%s&quot; দিয়ে কোয়েরি প্রতিস্থাপন করুন। উদাহরণ: \nhttps://www.google.com/search?q=%s</string>
google.sh:96:res/values-tr/strings.xml:1947: <string name="search_add_custom_engine_search_string_example" formatted="false">Sorguyu “%s” ile değiştirin. Örnek:\nhttps://www.google.com/search?q=%s</string>
google.sh:97:res/values-co/strings.xml:1982: <string name="search_add_custom_engine_search_string_example" formatted="false">Rimpiazzà i termi di a ricerca da « %s ». Esempiu :\nhttps://www.google.com/search?q=%s</string>

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,16 @@
import android.content.Context
import android.content.Intent
import android.net.Uri
import io.github.forkmaintainers.iceraven.components.getSafeString
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.webextension.InstallationMethod
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionRuntime
import mozilla.components.feature.intent.processing.IntentProcessor
import mozilla.components.support.ktx.android.net.getFileName
import org.json.JSONException
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.net.URLEncoder
import java.util.Base64
import java.util.zip.ZipFile
class AddonInstallIntentProcessor(private val context: Context, private val engine: Engine) : IntentProcessor {
class AddonInstallIntentProcessor(private val context: Context, private val runtime: WebExtensionRuntime) : IntentProcessor {
override fun process(intent: Intent): Boolean {
if(intent.data == null) {
return false
@ -25,39 +20,22 @@ class AddonInstallIntentProcessor(private val context: Context, private val engi
return false
}
val file = fromUri(iuri)
if(file == null) {
return false
}
val ext = file.let { parseExtension(it) }
installExtension(ext.get(0), ext.get(1), null)
val extURI = parseExtension(file)
installExtension(extURI) {}
return true
}
fun installExtension(id: String, b64: String, onSuccess: ((WebExtension) -> Unit)?) {
engine.installWebExtension(id, b64, if(onSuccess != null) {
onSuccess
} else {
{ }
})
fun installExtension(b64: String, onSuccess: ((WebExtension) -> Unit)) {
runtime.installWebExtension(b64, InstallationMethod.FROM_FILE, onSuccess)
}
fun parseExtension(inp: File): List<String> {
val file = ZipFile(inp)
val mis = file.getInputStream(file.getEntry("manifest.json"))
val t = org.json.JSONObject(String(mis.readBytes()))
val al = ArrayList<String>()
val bss = try {
t.getJSONObject("browser_specific_settings")
} catch(e:JSONException) {
t.getJSONObject("applications")
}
al.add(bss.getJSONObject("gecko").getSafeString("id") )
al.add(Uri.fromFile(inp.absoluteFile).toString())
file.close()
mis.close()
return al
fun parseExtension(inp: File): String {
return Uri.fromFile(inp.absoluteFile).toString()
}
fun fromUri(uri: Uri): File? {
fun fromUri(uri: Uri): File {
val name = uri.getFileName(context.contentResolver)
val file: File = File(context.externalCacheDir, name)
val file = File(context.externalCacheDir, name)
file.createNewFile()
val ostream = FileOutputStream(file.absolutePath)
val istream = context.contentResolver.openInputStream(uri)!!

View File

@ -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
@ -69,7 +70,9 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
private var addons: List<Addon> = emptyList()
private var adapter: AddonsManagerAdapter? = null
private var addonImportFilePicker: ActivityResultLauncher<Intent>? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
logger.info("View created for AddonsManagementFragment")
super.onViewCreated(view, savedInstanceState)
@ -85,25 +88,26 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
result: ActivityResult ->
if(result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let{uri ->
requireComponents.intentProcessors.addonInstallIntentProcessor.fromUri(uri)?.let{tmp ->
val ext = requireComponents.intentProcessors.addonInstallIntentProcessor.parseExtension(tmp)
requireComponents.intentProcessors.addonInstallIntentProcessor.installExtension(
ext[0], ext[1],
onSuccess = {
val ao = Addon.newFromWebExtension(it)
runIfFragmentIsAttached {
adapter?.updateAddon(ao)
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
requireComponents.intentProcessors.addonInstallIntentProcessor.fromUri(uri)
.let{ tmpFile ->
val extURI = requireComponents.intentProcessors.addonInstallIntentProcessor.parseExtension(tmpFile)
requireComponents.intentProcessors.addonInstallIntentProcessor.installExtension(
extURI,
onSuccess = {
val installedState = provideAddonManger().toInstalledState(it)
val ao = Addon.newFromWebExtension(it, installedState)
runIfFragmentIsAttached {
adapter?.updateAddon(ao)
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
}
}
}
)
}
)
}
}
}
}
}
private fun setupMenu() {
val menuHost = requireActivity() as MenuHost
@ -138,6 +142,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
showAlertDialog()
true
}
R.id.search -> {
true
}
@ -148,6 +153,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
viewLifecycleOwner, Lifecycle.State.RESUMED,
)
}
private fun installFromFile() {
val intent = Intent()
.setType("application/x-xpinstall")
@ -155,6 +161,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
addonImportFilePicker!!.launch(intent)
}
private fun showAlertDialog() {
val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext())
builder
@ -331,14 +338,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
},
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -122,6 +122,10 @@ class DynamicDownloadDialog(
(binding.root.layoutParams as CoordinatorLayout.LayoutParams).apply {
(behavior as DynamicDownloadDialogBehavior).forceExpand(binding.root)
}
if(!settings.shouldShowSuccessDownloadDialog && !didFail) {
dismiss()
}
}
private fun dismiss() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
*/

View File

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

View File

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

View File

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

View File

@ -183,7 +183,7 @@ fun createInitialSearchFragmentState(
showSyncedTabsSuggestionsForCurrentEngine = false,
showAllSyncedTabsSuggestions = settings.shouldShowSyncedTabsSuggestions,
showSessionSuggestionsForCurrentEngine = false,
showAllSessionSuggestions = true,
showAllSessionSuggestions = settings.shouldShowSessionSuggestions,
showSponsoredSuggestions = activity.browsingModeManager.mode == BrowsingMode.Normal &&
settings.enableFxSuggest && settings.showSponsoredSuggestions,
showNonSponsoredSuggestions = activity.browsingModeManager.mode == BrowsingMode.Normal &&
@ -284,7 +284,7 @@ private fun searchStateReducer(state: SearchFragmentState, action: SearchFragmen
action.settings.enableFxSuggest && action.settings.showSponsoredSuggestions,
showNonSponsoredSuggestions = action.browsingMode == BrowsingMode.Normal &&
action.settings.enableFxSuggest && action.settings.showNonSponsoredSuggestions,
showAllSessionSuggestions = true,
showAllSessionSuggestions = action.settings.shouldShowSessionSuggestions,
)
is SearchFragmentAction.SearchShortcutEngineSelected ->
state.copy(
@ -316,10 +316,10 @@ private fun searchStateReducer(state: SearchFragmentState, action: SearchFragmen
false -> action.settings.shouldShowSyncedTabsSuggestions
},
showSessionSuggestionsForCurrentEngine = action.settings.showUnifiedSearchFeature &&
!action.engine.isGeneral,
!action.engine.isGeneral && action.settings.shouldShowSessionSuggestions,
showAllSessionSuggestions = when (action.settings.showUnifiedSearchFeature) {
true -> false
false -> true
false -> action.settings.shouldShowSessionSuggestions
},
showSponsoredSuggestions = false,
showNonSponsoredSuggestions = false,

View File

@ -623,7 +623,7 @@ class AwesomeBarView(
// Maximum number of suggestions returned.
const val METADATA_SUGGESTION_LIMIT = 3
const val GOOGLE_SEARCH_ENGINE_NAME = "LeOSearch"
const val GOOGLE_SEARCH_ENGINE_NAME = "Google"
@VisibleForTesting
internal fun getDrawable(context: Context, resId: Int): Drawable? {

View File

@ -54,6 +54,7 @@ class CustomizationFragment : PreferenceFragmentCompat() {
setupRadioGroups()
setupToolbarCategory()
setupGesturesCategory()
setupDownloadCustomizationCategory()
setupAddonsCustomizationCategory()
setupSystemBehaviorCategory()
requirePreference<SwitchPreference>(R.string.pref_key_strip_url).apply {
@ -189,6 +190,13 @@ class CustomizationFragment : PreferenceFragmentCompat() {
}
}
private fun setupDownloadCustomizationCategory() {
requirePreference<SwitchPreference>(R.string.pref_key_success_download_dialog).apply {
isChecked = requireContext().settings().shouldShowSuccessDownloadDialog
onPreferenceChangeListener = SharedPreferenceUpdater()
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
resources.getString(R.string.pref_key_website_pull_to_refresh) -> {

View File

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

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