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 * `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). * 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 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. * **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. **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 // 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. // 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")) { if (gradle.hasProperty("localProperties.autosignReleaseWithDebugKey")) {
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
@ -198,6 +198,7 @@ android {
buildFeatures { buildFeatures {
viewBinding true viewBinding true
buildConfig true
} }
androidResources { androidResources {
@ -273,6 +274,9 @@ android {
excludes += ['META-INF/atomicfu.kotlin_module', 'META-INF/AL2.0', 'META-INF/LGPL2.1', excludes += ['META-INF/atomicfu.kotlin_module', 'META-INF/AL2.0', 'META-INF/LGPL2.1',
'META-INF/LICENSE.md', 'META-INF/LICENSE-notice.md'] '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*.*', def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*',
'**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*'] '**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*']
def kotlinDebugTree = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/${variant.name}", excludes: fileFilter) def kotlinDebugTree = fileTree(dir: "$project.layout.buildDirectory/tmp/kotlin-classes/${variant.name}", excludes: fileFilter)
def javaDebugTree = fileTree(dir: "$project.buildDir/intermediates/classes/${variant.flavorName}/${variant.buildType.name}", def javaDebugTree = fileTree(dir: "$project.layout.buildDirectory/intermediates/classes/${variant.flavorName}/${variant.buildType.name}",
excludes: fileFilter) excludes: fileFilter)
def mainSrc = "$project.projectDir/src/main/java" def mainSrc = "$project.projectDir/src/main/java"
sourceDirectories.setFrom(files([mainSrc])) sourceDirectories.setFrom(files([mainSrc]))
classDirectories.setFrom(files([kotlinDebugTree, javaDebugTree])) 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", "jacoco/test${variant.name.capitalize()}UnitTest.exec",
'outputs/code-coverage/connected/*coverage.ec' '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: metadata:
tags: tags:
- PrivateBrowsing - 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: synced_tab_opened:
type: event type: event
description: | description: |
@ -468,6 +489,39 @@ events:
notification_emails: notification_emails:
- android-probes@mozilla.com - android-probes@mozilla.com
expires: never 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: tab_view_changed:
type: event type: event
description: | description: |
@ -1033,7 +1087,7 @@ onboarding:
set_to_default_card: set_to_default_card:
type: event type: event
description: | description: |
User viewed juno onboarding set to default card. User viewed onboarding set to default card.
extra_keys: extra_keys:
element_type: element_type:
type: string type: string
@ -1067,7 +1121,7 @@ onboarding:
sign_in_card: sign_in_card:
type: event type: event
description: | description: |
User viewed juno onboarding sign in card. User viewed onboarding sign in card.
extra_keys: extra_keys:
element_type: element_type:
type: string type: string
@ -1101,7 +1155,7 @@ onboarding:
turn_on_notifications_card: turn_on_notifications_card:
type: event type: event
description: | description: |
User viewed juno onboarding notification permission card. User viewed onboarding notification permission card.
extra_keys: extra_keys:
element_type: element_type:
type: string type: string
@ -1135,7 +1189,7 @@ onboarding:
set_to_default: set_to_default:
type: event type: event
description: | description: |
User tapped on set to default button in juno onboarding. User tapped on set to default button in onboarding.
extra_keys: extra_keys:
element_type: element_type:
type: string type: string
@ -1169,7 +1223,7 @@ onboarding:
skip_default: skip_default:
type: event type: event
description: | description: |
User tapped on skip set to default button in juno onboarding. User tapped on skip set to default button in onboarding.
extra_keys: extra_keys:
element_type: element_type:
type: string type: string
@ -1203,7 +1257,7 @@ onboarding:
sign_in: sign_in:
type: event type: event
description: | description: |
User tapped on sign in button in juno onboarding. User tapped on sign in button in onboarding.
extra_keys: extra_keys:
element_type: element_type:
type: string type: string
@ -1237,7 +1291,7 @@ onboarding:
skip_sign_in: skip_sign_in:
type: event type: event
description: | description: |
User tapped on skip sign in button in juno onboarding. User tapped on skip sign in button in onboarding.
extra_keys: extra_keys:
element_type: element_type:
type: string type: string
@ -1271,7 +1325,7 @@ onboarding:
turn_on_notifications: turn_on_notifications:
type: event type: event
description: | description: |
User tapped on turn on notifications button in juno onboarding. User tapped on turn on notifications button in onboarding.
extra_keys: extra_keys:
element_type: element_type:
type: string type: string
@ -1305,7 +1359,7 @@ onboarding:
skip_turn_on_notifications: skip_turn_on_notifications:
type: event type: event
description: | description: |
User tapped on skip turn on notification button in juno onboarding. User tapped on skip turn on notification button in onboarding.
extra_keys: extra_keys:
element_type: element_type:
type: string type: string
@ -1339,7 +1393,7 @@ onboarding:
add_search_widget_card: add_search_widget_card:
type: event type: event
description: | description: |
User viewed juno onboarding add search widget card. User viewed onboarding add search widget card.
extra_keys: extra_keys:
element_type: element_type:
type: string type: string
@ -1373,7 +1427,7 @@ onboarding:
add_search_widget: add_search_widget:
type: event type: event
description: | description: |
User tapped on Add Firefox Widget in juno onboarding. User tapped on Add Firefox Widget in onboarding.
extra_keys: extra_keys:
element_type: element_type:
type: string type: string
@ -1407,7 +1461,7 @@ onboarding:
skip_add_search_widget: skip_add_search_widget:
type: event type: event
description: | description: |
User tapped on skip add search widget button in juno onboarding. User tapped on skip add search widget button in onboarding.
extra_keys: extra_keys:
element_type: element_type:
type: string type: string
@ -1441,7 +1495,7 @@ onboarding:
privacy_policy: privacy_policy:
type: event type: event
description: | description: |
User tapped on privacy policy link in juno onboarding. User tapped on privacy policy link in onboarding.
extra_keys: extra_keys:
element_type: element_type:
type: string type: string
@ -1475,7 +1529,7 @@ onboarding:
completed: completed:
type: event type: event
description: | description: |
User completed the juno onboarding. User completed onboarding.
extra_keys: extra_keys:
sequence_position: sequence_position:
type: string type: string
@ -2612,6 +2666,25 @@ metrics:
metadata: metadata:
tags: tags:
- Experiments - 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: customize_home:
most_visited_sites: most_visited_sites:
@ -9088,6 +9161,104 @@ awesomebar:
metadata: metadata:
tags: tags:
- Search - 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: android_autofill:
supported: supported:
type: boolean type: boolean

View File

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

View File

@ -2,7 +2,7 @@
features: features:
juno-onboarding: juno-onboarding:
description: A feature that shows juno onboarding flow. description: A feature that shows the onboarding flow.
variables: variables:
conditions: conditions:
@ -10,14 +10,16 @@ features:
A collection of out the box conditional expressions to be A collection of out the box conditional expressions to be
used in determining whether a card should show or not. used in determining whether a card should show or not.
Each entry maps to a valid JEXL expression. Each entry maps to a valid JEXL expression.
type: Map<String, String> type: Map<ConditionName, String>
string-alias: ConditionName
default: { default: {
ALWAYS: "true", ALWAYS: "true",
NEVER: "false" NEVER: "false"
} }
cards: cards:
description: Collection of user facing onboarding cards. description: Collection of user facing onboarding cards.
type: Map<String, OnboardingCardData> type: Map<OnboardingCardKey, OnboardingCardData>
string-alias: OnboardingCardKey
default: default:
default-browser: default-browser:
card-type: default-browser card-type: default-browser
@ -109,7 +111,7 @@ objects:
# This should never be defaulted. # This should never be defaulted.
default: "" default: ""
prerequisites: prerequisites:
type: List<String> type: List<ConditionName>
description: > description: >
A list of strings corresponding to targeting expressions. A list of strings corresponding to targeting expressions.
The card will be shown if all expressions are `true` and if The card will be shown if all expressions are `true` and if
@ -117,7 +119,7 @@ objects:
if the `disqualifiers` table is empty. if the `disqualifiers` table is empty.
default: [ ALWAYS ] default: [ ALWAYS ]
disqualifiers: disqualifiers:
type: List<String> type: List<ConditionName>
description: > description: >
A list of strings corresponding to targeting expressions. A list of strings corresponding to targeting expressions.
The card will not be shown if any expression is `true`. 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 - https://github.com/mozilla-mobile/firefox-android/pull/1298#pullrequestreview-1350344223
notification_emails: notification_emails:
- android-probes@mozilla.com - android-probes@mozilla.com
fx-suggest: fx-suggest:
description: | description: |
A ping representing a single event occurring with or to a Firefox Suggestion. A ping representing a single event occurring with or to a Firefox Suggestion.
@ -91,3 +92,15 @@ fx-suggest:
- lina@mozilla.com - lina@mozilla.com
- ttran@mozilla.com - ttran@mozilla.com
- najiang@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 # Keep Android Lifecycle methods
# https://bugzilla.mozilla.org/show_bug.cgi?id=1596302 # https://bugzilla.mozilla.org/show_bug.cgi?id=1596302
-keep class androidx.lifecycle.** { *; } -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") @pytest.fixture(name="experiment_data")
def fixture_experiment_data(experiment_url): def fixture_experiment_data(experiment_url):
data = requests.get(experiment_url).json() data = requests.get(experiment_url).json()
for item in data["branches"][0]["features"][0]["value"]["messages"].values(): branches = next(iter(data.get("branches")), None)
item["surface"] = "homescreen" features = next(iter(branches.get("features")), None)
item["style"] = "URGENT" if features.get("messages"):
for count, trigger in enumerate(item["trigger"]): for item in features["value"]["messages"].values():
if "USER_EN_SPEAKER" not in trigger: item["surface"] = "homescreen"
del item["trigger"][count] item["style"] = "URGENT"
for count, trigger in enumerate(item["trigger"]):
if "USER_EN_SPEAKER" not in trigger:
del item["trigger"][count]
return [data] return [data]

View File

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

View File

@ -1,17 +1,25 @@
import pytest import pytest
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True) @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) setup_experiment(load_branches)
gradlewbuild.test("GenericExperimentIntegrationTest#disableStudiesViaStudiesToggle") gradlewbuild.test("GenericExperimentIntegrationTest#disableStudiesViaStudiesToggle")
assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0]) assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolled") gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolled")
assert check_ping_for_experiment(reason="unenrollment", branch=load_branches[0]) assert check_ping_for_experiment(reason="unenrollment", branch=load_branches[0])
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True) @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) setup_experiment(load_branches)
gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolledViaSecretMenu") gradlewbuild.test(
"GenericExperimentIntegrationTest#testExperimentUnenrolledViaSecretMenu"
)
assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0]) assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolled") gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolled")
assert check_ping_for_experiment(reason="unenrollment", branch=load_branches[0]) 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.Config
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity 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.PackageName.YOUTUBE_APP
import org.mozilla.fenix.helpers.Constants.TAG 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.TestHelper.mDevice
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.idlingresource.NetworkConnectionIdlingResource import org.mozilla.fenix.helpers.idlingresource.NetworkConnectionIdlingResource
@ -295,12 +297,14 @@ object AppAndSystemHelper {
) )
} }
fun bringAppToForeground() { /**
mDevice.pressRecentApps() * Brings the app to foregorund by clicking it in the recent apps tray.
mDevice.findObject(UiSelector().resourceId("${TestHelper.packageName}:id/container")).waitForExists( * The package name is related to the home screen experience for the Pixel phones produced by Google.
TestAssetHelper.waitingTime, * 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) { fun verifyKeyboardVisibility(isExpectedToBeVisible: Boolean = true) {
mDevice.waitForIdle() mDevice.waitForIdle()

View File

@ -22,6 +22,7 @@ object Constants {
const val PHONE_APP = "com.android.dialer" const val PHONE_APP = "com.android.dialer"
const val ANDROID_SETTINGS = "com.android.settings" const val ANDROID_SETTINGS = "com.android.settings"
const val PRINT_SPOOLER = "com.android.printspooler" 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" const val SPEECH_RECOGNITION = "android.speech.action.RECOGNIZE_SPEECH"

View File

@ -146,4 +146,10 @@ object TestAssetHelper {
return TestAsset(url, "", "") 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.OnboardingCardData
import org.mozilla.fenix.nimbus.OnboardingCardType import org.mozilla.fenix.nimbus.OnboardingCardType
class JunoOnboardingMapperTest { class OnboardingMapperTest {
@get:Rule @get:Rule
val activityTestRule = val activityTestRule =

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -80,6 +80,8 @@ class EnhancedTrackingProtectionTest {
verifyEnhancedTrackingProtectionLevelSelected("Standard (default)", true) verifyEnhancedTrackingProtectionLevelSelected("Standard (default)", true)
verifyStandardOptionDescription() verifyStandardOptionDescription()
verifyStrictOptionDescription() verifyStrictOptionDescription()
verifyGPCTextWithSwitchWidget()
verifyGPCSwitchEnabled(false)
selectTrackingProtectionOption("Custom") selectTrackingProtectionOption("Custom")
verifyCustomTrackingProtectionSettings() verifyCustomTrackingProtectionSettings()
scrollToElementByText("Standard (default)") 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 okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
@ -156,6 +155,7 @@ class HomeScreenTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.goToHomescreen { }.goToHomescreen {
}.openCustomizeHomepage { }.openCustomizeHomepage {
clickShortcutsButton()
clickJumpBackInButton() clickJumpBackInButton()
clickRecentBookmarksButton() clickRecentBookmarksButton()
clickRecentSearchesButton() clickRecentSearchesButton()
@ -164,14 +164,13 @@ class HomeScreenTest {
verifyCustomizeHomepageButton(false) verifyCustomizeHomepageButton(false)
}.openThreeDotMenu { }.openThreeDotMenu {
}.openCustomizeHome { }.openCustomizeHome {
clickJumpBackInButton() clickShortcutsButton()
}.goBackToHomeScreen { }.goBackToHomeScreen {
verifyCustomizeHomepageButton(true) verifyCustomizeHomepageButton(true)
} }
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/414970 // 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 @SmokeTest
@Test @Test
fun addPrivateBrowsingShortcutFromHomeScreenCFRTest() { fun addPrivateBrowsingShortcutFromHomeScreenCFRTest() {

View File

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

View File

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

View File

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

View File

@ -321,7 +321,6 @@ class SettingsSearchTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203312 // 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 @Test
fun verifyErrorMessagesForInvalidSearchEngineUrlsTest() { fun verifyErrorMessagesForInvalidSearchEngineUrlsTest() {
val customSearchEngine = object { val customSearchEngine = object {
@ -420,7 +419,6 @@ class SettingsSearchTest {
// Test running on beta/release builds in CI: // Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds // 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. // 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 @SmokeTest
@Test @Test
fun verifyShowSearchSuggestionsToggleTest() { fun verifyShowSearchSuggestionsToggleTest() {

View File

@ -450,7 +450,6 @@ class SettingsSitePermissionsTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1923417 // 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 @Test
fun verifyDRMControlledContentPermissionSettingsTest() { fun verifyDRMControlledContentPermissionSettingsTest() {
navigationToolbar { navigationToolbar {

View File

@ -15,7 +15,6 @@ import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Assume.assumeTrue import org.junit.Assume.assumeTrue
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
@ -98,7 +97,6 @@ class SitePermissionsTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334294 // 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 @Test
fun blockAudioVideoPermissionRememberingTheDecisionTest() { fun blockAudioVideoPermissionRememberingTheDecisionTest() {
assumeTrue(cameraManager.cameraIdList.isNotEmpty()) assumeTrue(cameraManager.cameraIdList.isNotEmpty())
@ -122,7 +120,6 @@ class SitePermissionsTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251388 // 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 @Test
fun allowAudioVideoPermissionRememberingTheDecisionTest() { fun allowAudioVideoPermissionRememberingTheDecisionTest() {
assumeTrue(cameraManager.cameraIdList.isNotEmpty()) assumeTrue(cameraManager.cameraIdList.isNotEmpty())
@ -164,7 +161,6 @@ class SitePermissionsTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334190 // 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 @Test
fun blockMicrophonePermissionRememberingTheDecisionTest() { fun blockMicrophonePermissionRememberingTheDecisionTest() {
assumeTrue(micManager.microphones.isNotEmpty()) assumeTrue(micManager.microphones.isNotEmpty())
@ -187,7 +183,6 @@ class SitePermissionsTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251387 // 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 @Test
fun allowMicrophonePermissionRememberingTheDecisionTest() { fun allowMicrophonePermissionRememberingTheDecisionTest() {
assumeTrue(micManager.microphones.isNotEmpty()) assumeTrue(micManager.microphones.isNotEmpty())
@ -228,7 +223,6 @@ class SitePermissionsTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334077 // 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 @Test
fun blockCameraPermissionRememberingTheDecisionTest() { fun blockCameraPermissionRememberingTheDecisionTest() {
assumeTrue(cameraManager.cameraIdList.isNotEmpty()) assumeTrue(cameraManager.cameraIdList.isNotEmpty())
@ -251,7 +245,6 @@ class SitePermissionsTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251386 // 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 @Test
fun allowCameraPermissionRememberingTheDecisionTest() { fun allowCameraPermissionRememberingTheDecisionTest() {
assumeTrue(cameraManager.cameraIdList.isNotEmpty()) 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.longClick
import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.performTouchInput
@ -33,8 +32,10 @@ import org.mozilla.fenix.home.topsites.TopSitesTestTag
*/ */
class ComposeTopSitesRobot(private val composeTestRule: HomeActivityComposeTestRule) { class ComposeTopSitesRobot(private val composeTestRule: HomeActivityComposeTestRule) {
fun verifyExistingTopSitesList() = @OptIn(ExperimentalTestApi::class)
composeTestRule.onNodeWithTag(TopSitesTestTag.topSites).assertExists() fun verifyExistingTopSitesList() {
composeTestRule.waitUntilExactlyOneExists(hasTestTag(TopSitesTestTag.topSites), timeoutMillis = waitingTime)
}
@OptIn(ExperimentalTestApi::class) @OptIn(ExperimentalTestApi::class)
fun verifyExistingTopSiteItem(vararg titles: String) { fun verifyExistingTopSiteItem(vararg titles: String) {

View File

@ -6,6 +6,7 @@
package org.mozilla.fenix.ui.robots package org.mozilla.fenix.ui.robots
import android.util.Log
import androidx.compose.ui.test.ComposeTimeoutException import androidx.compose.ui.test.ComposeTimeoutException
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertAny 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.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort 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.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.waitForObjects 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() rule.waitForIdle()
for (i in 1..RETRY_COUNT) { for (i in 1..RETRY_COUNT) {
try { try {
@ -99,6 +107,9 @@ class SearchRobot {
homeScreen { homeScreen {
}.openSearch { }.openSearch {
typeSearch(searchTerm) 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 { class Transition {
private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource

View File

@ -55,6 +55,9 @@ class SettingsSubMenuAddonsManagerRobot {
fun verifyAddonsListIsDisplayed(shouldBeDisplayed: Boolean) = fun verifyAddonsListIsDisplayed(shouldBeDisplayed: Boolean) =
assertUIObjectExists(addonsList(), exists = shouldBeDisplayed) assertUIObjectExists(addonsList(), exists = shouldBeDisplayed)
fun verifyAddonDownloadOverlay() =
onView(withText(R.string.mozac_add_on_install_progress_caption)).check(matches(isDisplayed()))
fun verifyAddonPermissionPrompt(addonName: String) { fun verifyAddonPermissionPrompt(addonName: String) {
mDevice.waitNotNull(Until.findObject(By.text("Add $addonName?")), waitingTime) 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.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.Visibility 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.isChecked
import org.mozilla.fenix.helpers.isEnabled 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. * Implementation of Robot Pattern for the settings Enhanced Tracking Protection sub menu.
*/ */
@ -66,6 +69,33 @@ class SettingsSubMenuEnhancedTrackingProtectionRobot {
), ),
).click() ).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() { fun verifyStandardOptionDescription() {
onView(withText(R.string.preference_enhanced_tracking_protection_standard_description_5)) onView(withText(R.string.preference_enhanced_tracking_protection_standard_description_5))
.check(matches(isDisplayed())) .check(matches(isDisplayed()))

View File

@ -119,7 +119,7 @@ class SitePermissionsRobot {
fun verifyDRMContentPermissionPrompt(url: String) { fun verifyDRMContentPermissionPrompt(url: String) {
try { 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(denyPagePermissionButton, expectedText = "Dont allow")
assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow") assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow")
} catch (e: AssertionError) { } catch (e: AssertionError) {
@ -127,7 +127,7 @@ class SitePermissionsRobot {
}.openThreeDotMenu { }.openThreeDotMenu {
}.refreshPage { }.refreshPage {
}.clickRequestDRMControlledContentAccessButton { }.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(denyPagePermissionButton, expectedText = "Dont allow")
assertItemTextEquals(allowPagePermissionButton, expectedText = "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_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" /> <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="android.permission.RECORD_AUDIO" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
@ -43,7 +53,6 @@
<application <application
android:name=".FenixApplication" android:name=".FenixApplication"
android:allowBackup="false" android:allowBackup="false"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
@ -266,6 +275,14 @@
<data android:scheme="https" /> <data android:scheme="https" />
</intent-filter> </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 <meta-data
android:name="com.android.systemui.action_assist_icon" android:name="com.android.systemui.action_assist_icon"
android:resource="@mipmap/ic_launcher" /> 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", 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://leosearch.ddns.net/search?q=%s</string> 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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://www.google.com/search?q=%s</string>
res/values-cak/strings.xml:1787:Achi\'el: \nhttps://leosearch.ddns.net/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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://www.google.com/search?q=%s</string>
res/values-skr/strings.xml:1847:مثال:\nhttps://leosearch.ddns.net/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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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) this.navController = WeakReference(navController)
} }
override fun interceptsAppInitiatedRequests() = true
override fun onLoadRequest( override fun onLoadRequest(
engineSession: EngineSession, engineSession: EngineSession,
uri: String, uri: String,
@ -37,17 +39,27 @@ class AppRequestInterceptor(
isDirectNavigation: Boolean, isDirectNavigation: Boolean,
isSubframeRequest: Boolean, isSubframeRequest: Boolean,
): RequestInterceptor.InterceptionResponse? { ): RequestInterceptor.InterceptionResponse? {
return context.components.services.appLinksInterceptor val services = context.components.services
.onLoadRequest(
engineSession, return services.urlRequestInterceptor.onLoadRequest(
uri, engineSession,
lastUri, uri,
hasUserGesture, lastUri,
isSameDomain, hasUserGesture,
isRedirect, isSameDomain,
isDirectNavigation, isRedirect,
isSubframeRequest, isDirectNavigation,
) isSubframeRequest,
) ?: services.appLinksInterceptor.onLoadRequest(
engineSession,
uri,
lastUri,
hasUserGesture,
isSameDomain,
isRedirect,
isDirectNavigation,
isSubframeRequest,
)
} }
override fun onErrorRequest( override fun onErrorRequest(

View File

@ -78,4 +78,14 @@ object FeatureFlags {
* Enable Meta attribution. * Enable Meta attribution.
*/ */
const val metaAttributionEnabled = true 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, enableEventTimestamps = FxNimbus.features.glean.value().enableEventTimestamps,
) )
// Set the metric configuration from Nimbus.
Glean.setMetricsEnabledConfig(FxNimbus.features.glean.value().metricsEnabled)
Glean.initialize( Glean.initialize(
applicationContext = this, applicationContext = this,
configuration = configuration.setCustomEndpointIfAvailable(customEndpoint), configuration = configuration.setCustomEndpointIfAvailable(customEndpoint),
@ -214,9 +217,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
buildInfo = GleanBuildInfo.buildInfo, 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. // We avoid blocking the main thread on startup by setting startup metrics on the background thread.
val store = components.core.store val store = components.core.store
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {

View File

@ -46,6 +46,7 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.state.action.ContentAction 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.appstate.AppAction
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.components.metrics.GrowthDataWorker 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.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.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment
import org.mozilla.fenix.ext.alreadyOnDestination 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.TabsTrayFragment
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager 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.theme.ThemeManager
import org.mozilla.fenix.trackingprotection.TrackingProtectionPanelDialogFragmentDirections import org.mozilla.fenix.trackingprotection.TrackingProtectionPanelDialogFragmentDirections
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@ -277,6 +284,36 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
window.decorView.layoutDirection = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) window.decorView.layoutDirection = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
binding = ActivityHomeBinding.inflate(layoutInflater) 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) setContentView(binding.root)
ProfilerMarkers.addListenerForOnGlobalLayout(components.core.engine, this, binding.root) ProfilerMarkers.addListenerForOnGlobalLayout(components.core.engine, this, binding.root)
@ -294,14 +331,14 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
it.start() it.start()
} }
if (settings().shouldShowJunoOnboarding( if (settings().shouldShowOnboarding(
hasUserBeenOnboarded = components.fenixOnboarding.userHasBeenOnboarded(), hasUserBeenOnboarded = components.fenixOnboarding.userHasBeenOnboarded(),
isLauncherIntent = intent.toSafeIntent().isLauncherIntent, isLauncherIntent = intent.toSafeIntent().isLauncherIntent,
) )
) { ) {
// Unless activity is recreated due to config change, navigate to onboarding // Unless activity is recreated due to config change, navigate to onboarding
if (savedInstanceState == null) { if (savedInstanceState == null) {
navHost.navController.navigate(NavGraphDirections.actionGlobalJunoOnboarding()) navHost.navController.navigate(NavGraphDirections.actionGlobalOnboarding())
} }
} else { } else {
lifecycleScope.launch(IO) { lifecycleScope.launch(IO) {
@ -521,6 +558,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
GrowthDataWorker.sendActivatedSignalIfNeeded(applicationContext) GrowthDataWorker.sendActivatedSignalIfNeeded(applicationContext)
FontEnumerationWorker.sendActivatedSignalIfNeeded(applicationContext)
ReEngagementNotificationWorker.setReEngagementNotificationIfNeeded(applicationContext) ReEngagementNotificationWorker.setReEngagementNotificationIfNeeded(applicationContext)
MessageNotificationWorker.setMessageNotificationWorker(applicationContext) MessageNotificationWorker.setMessageNotificationWorker(applicationContext)
} }
@ -570,17 +608,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.core.store.state.getNormalOrPrivateTabs(private = false).isNotEmpty() components.core.store.state.getNormalOrPrivateTabs(private = false).isNotEmpty()
lifecycleScope.launch(IO) { lifecycleScope.launch(IO) {
components.core.bookmarksStorage.getTree(BookmarkRoot.Root.id, true)?.let { val desktopFolders = DesktopFolders(
val desktopRootNode = DesktopFolders( applicationContext,
applicationContext, showMobileRoot = false,
showMobileRoot = false, )
).withOptionalDesktopFolders(it) settings().desktopBookmarksSize = desktopFolders.count()
settings().desktopBookmarksSize = desktopRootNode.count()
}
components.core.bookmarksStorage.getTree(BookmarkRoot.Mobile.id, true)?.let { settings().mobileBookmarksSize = components.core.bookmarksStorage.countBookmarksInTrees(
settings().mobileBookmarksSize = it.count() listOf(BookmarkRoot.Mobile.id),
} ).toInt()
} }
super.onPause() super.onPause()
@ -626,7 +662,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.core.pocketStoriesService.stopPeriodicSponsoredStoriesRefresh() components.core.pocketStoriesService.stopPeriodicSponsoredStoriesRefresh()
privateNotificationObserver?.stop() privateNotificationObserver?.stop()
components.notificationsDelegate.unBindActivity(this) components.notificationsDelegate.unBindActivity(this)
stopMediaSession()
if (this !is ExternalAppBrowserActivity) {
stopMediaSession()
}
} }
override fun onConfigurationChanged(newConfig: Configuration) { 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.feature.intent.processing.IntentProcessor
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_CATEGORY import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_CATEGORY
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_PACKAGE 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 mozilla.components.support.utils.ext.getApplicationInfoCompat
import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity.Companion.PRIVATE_BROWSING_MODE import org.mozilla.fenix.HomeActivity.Companion.PRIVATE_BROWSING_MODE
@ -73,6 +74,12 @@ class IntentReceiverActivity : Activity() {
addReferrerInformation(intent) 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 processor = getIntentProcessors(private).firstOrNull { it.process(intent) }
val intentProcessorType = components.intentProcessors.getType(processor) val intentProcessorType = components.intentProcessors.getType(processor)

View File

@ -1,21 +1,16 @@
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import io.github.forkmaintainers.iceraven.components.getSafeString import mozilla.components.concept.engine.webextension.InstallationMethod
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.webextension.WebExtension import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionRuntime
import mozilla.components.feature.intent.processing.IntentProcessor import mozilla.components.feature.intent.processing.IntentProcessor
import mozilla.components.support.ktx.android.net.getFileName import mozilla.components.support.ktx.android.net.getFileName
import org.json.JSONException
import org.json.JSONObject
import java.io.File import java.io.File
import java.io.FileOutputStream 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 { override fun process(intent: Intent): Boolean {
if(intent.data == null) { if(intent.data == null) {
return false return false
@ -25,39 +20,22 @@ class AddonInstallIntentProcessor(private val context: Context, private val engi
return false return false
} }
val file = fromUri(iuri) val file = fromUri(iuri)
if(file == null) { val extURI = parseExtension(file)
return false installExtension(extURI) {}
}
val ext = file.let { parseExtension(it) }
installExtension(ext.get(0), ext.get(1), null)
return true return true
} }
fun installExtension(id: String, b64: String, onSuccess: ((WebExtension) -> Unit)?) {
engine.installWebExtension(id, b64, if(onSuccess != null) { fun installExtension(b64: String, onSuccess: ((WebExtension) -> Unit)) {
onSuccess runtime.installWebExtension(b64, InstallationMethod.FROM_FILE, onSuccess)
} else {
{ }
})
} }
fun parseExtension(inp: File): List<String> {
val file = ZipFile(inp) fun parseExtension(inp: File): String {
val mis = file.getInputStream(file.getEntry("manifest.json")) return Uri.fromFile(inp.absoluteFile).toString()
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 fromUri(uri: Uri): File? {
fun fromUri(uri: Uri): File {
val name = uri.getFileName(context.contentResolver) val name = uri.getFileName(context.contentResolver)
val file: File = File(context.externalCacheDir, name) val file = File(context.externalCacheDir, name)
file.createNewFile() file.createNewFile()
val ostream = FileOutputStream(file.absolutePath) val ostream = FileOutputStream(file.absolutePath)
val istream = context.contentResolver.openInputStream(uri)!! 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
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.concept.engine.webextension.InstallationMethod
import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManager import mozilla.components.feature.addons.AddonManager
import mozilla.components.feature.addons.AddonManagerException 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 addons: List<Addon> = emptyList()
private var adapter: AddonsManagerAdapter? = null private var adapter: AddonsManagerAdapter? = null
private var addonImportFilePicker: ActivityResultLauncher<Intent>? = null private var addonImportFilePicker: ActivityResultLauncher<Intent>? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
logger.info("View created for AddonsManagementFragment") logger.info("View created for AddonsManagementFragment")
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -85,25 +88,26 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
result: ActivityResult -> result: ActivityResult ->
if(result.resultCode == Activity.RESULT_OK) { if(result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let{uri -> result.data?.data?.let{uri ->
requireComponents.intentProcessors.addonInstallIntentProcessor.fromUri(uri)?.let{tmp -> requireComponents.intentProcessors.addonInstallIntentProcessor.fromUri(uri)
val ext = requireComponents.intentProcessors.addonInstallIntentProcessor.parseExtension(tmp) .let{ tmpFile ->
requireComponents.intentProcessors.addonInstallIntentProcessor.installExtension( val extURI = requireComponents.intentProcessors.addonInstallIntentProcessor.parseExtension(tmpFile)
ext[0], ext[1], requireComponents.intentProcessors.addonInstallIntentProcessor.installExtension(
onSuccess = { extURI,
val ao = Addon.newFromWebExtension(it) onSuccess = {
runIfFragmentIsAttached { val installedState = provideAddonManger().toInstalledState(it)
adapter?.updateAddon(ao) val ao = Addon.newFromWebExtension(it, installedState)
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE runIfFragmentIsAttached {
adapter?.updateAddon(ao)
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
}
} }
} )
) }
}
} }
} }
} }
} }
private fun setupMenu() { private fun setupMenu() {
val menuHost = requireActivity() as MenuHost val menuHost = requireActivity() as MenuHost
@ -138,6 +142,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
showAlertDialog() showAlertDialog()
true true
} }
R.id.search -> { R.id.search -> {
true true
} }
@ -148,6 +153,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
viewLifecycleOwner, Lifecycle.State.RESUMED, viewLifecycleOwner, Lifecycle.State.RESUMED,
) )
} }
private fun installFromFile() { private fun installFromFile() {
val intent = Intent() val intent = Intent()
.setType("application/x-xpinstall") .setType("application/x-xpinstall")
@ -155,6 +161,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
addonImportFilePicker!!.launch(intent) addonImportFilePicker!!.launch(intent)
} }
private fun showAlertDialog() { private fun showAlertDialog() {
val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext()) val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext())
builder builder
@ -331,14 +338,15 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
binding?.let { announceForAccessibility(it.addonProgressOverlay.addOnsOverlayText.text) } binding?.let { announceForAccessibility(it.addonProgressOverlay.addOnsOverlayText.text) }
} }
val installOperation = provideAddonManger().installAddon( val installOperation = provideAddonManger().installAddon(
addon, url = addon.downloadUrl,
installationMethod = InstallationMethod.MANAGER,
onSuccess = { onSuccess = {
runIfFragmentIsAttached { runIfFragmentIsAttached {
adapter?.updateAddon(it) adapter?.updateAddon(it)
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
} }
}, },
onError = { _, _ -> onError = { _ ->
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE 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.AddonManager
import mozilla.components.feature.addons.AddonManagerException import mozilla.components.feature.addons.AddonManagerException
import mozilla.components.feature.addons.ui.translateName import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentInstalledAddOnDetailsBinding import org.mozilla.fenix.databinding.FragmentInstalledAddOnDetailsBinding
@ -37,8 +38,11 @@ import org.mozilla.fenix.ext.showToolbar
class InstalledAddonDetailsFragment : Fragment() { class InstalledAddonDetailsFragment : Fragment() {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal lateinit var addon: Addon internal lateinit var addon: Addon
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val binding get() = _binding!!
private var _binding: FragmentInstalledAddOnDetailsBinding? = null private var _binding: FragmentInstalledAddOnDetailsBinding? = null
private val binding get() = _binding!!
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -49,14 +53,14 @@ class InstalledAddonDetailsFragment : Fragment() {
addon = AddonDetailsFragmentArgs.fromBundle(requireNotNull(arguments)).addon addon = AddonDetailsFragmentArgs.fromBundle(requireNotNull(arguments)).addon
} }
_binding = FragmentInstalledAddOnDetailsBinding.inflate( setBindingAndBindUI(
inflater, FragmentInstalledAddOnDetailsBinding.inflate(
container, inflater,
false, container,
false,
),
) )
bindUI()
return binding.root return binding.root
} }
@ -77,6 +81,12 @@ class InstalledAddonDetailsFragment : Fragment() {
_binding = null _binding = null
} }
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun setBindingAndBindUI(binding: FragmentInstalledAddOnDetailsBinding) {
_binding = binding
bindUI()
}
private fun bindAddon() { private fun bindAddon() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
@ -116,6 +126,7 @@ class InstalledAddonDetailsFragment : Fragment() {
bindPermissions() bindPermissions()
bindAllowInPrivateBrowsingSwitch() bindAllowInPrivateBrowsingSwitch()
bindRemoveButton() bindRemoveButton()
bindReportButton()
} }
@VisibleForTesting @VisibleForTesting
@ -143,7 +154,7 @@ class InstalledAddonDetailsFragment : Fragment() {
switch.setOnCheckedChangeListener { v, isChecked -> switch.setOnCheckedChangeListener { v, isChecked ->
val addonManager = v.context.components.addonManager val addonManager = v.context.components.addonManager
switch.isClickable = false switch.isClickable = false
binding.removeAddOn.isEnabled = false disableButtons()
if (isChecked) { if (isChecked) {
enableAddon( enableAddon(
addonManager, addonManager,
@ -155,7 +166,7 @@ class InstalledAddonDetailsFragment : Fragment() {
privateBrowsingSwitch.isChecked = it.isAllowedInPrivateBrowsing() privateBrowsingSwitch.isChecked = it.isAllowedInPrivateBrowsing()
switch.setText(R.string.mozac_feature_addons_enabled) switch.setText(R.string.mozac_feature_addons_enabled)
binding.settings.isVisible = shouldSettingsBeVisible() binding.settings.isVisible = shouldSettingsBeVisible()
binding.removeAddOn.isEnabled = true enableButtons()
context?.let { context?.let {
showSnackBar( showSnackBar(
binding.root, binding.root,
@ -170,7 +181,7 @@ class InstalledAddonDetailsFragment : Fragment() {
onError = { onError = {
runIfFragmentIsAttached { runIfFragmentIsAttached {
switch.isClickable = true switch.isClickable = true
binding.removeAddOn.isEnabled = true enableButtons()
switch.setState(addon.isEnabled()) switch.setState(addon.isEnabled())
context?.let { context?.let {
showSnackBar( showSnackBar(
@ -194,7 +205,7 @@ class InstalledAddonDetailsFragment : Fragment() {
switch.isClickable = true switch.isClickable = true
privateBrowsingSwitch.isVisible = it.isEnabled() privateBrowsingSwitch.isVisible = it.isEnabled()
switch.setText(R.string.mozac_feature_addons_disabled) switch.setText(R.string.mozac_feature_addons_disabled)
binding.removeAddOn.isEnabled = true enableButtons()
context?.let { context?.let {
showSnackBar( showSnackBar(
binding.root, binding.root,
@ -210,7 +221,7 @@ class InstalledAddonDetailsFragment : Fragment() {
runIfFragmentIsAttached { runIfFragmentIsAttached {
switch.isClickable = true switch.isClickable = true
privateBrowsingSwitch.isClickable = true privateBrowsingSwitch.isClickable = true
binding.removeAddOn.isEnabled = true enableButtons()
switch.setState(addon.isEnabled()) switch.setState(addon.isEnabled())
context?.let { context?.let {
showSnackBar( 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() { private fun bindSettings() {
binding.settings.apply { binding.settings.apply {
isVisible = shouldSettingsBeVisible() isVisible = shouldSettingsBeVisible()
@ -304,7 +332,7 @@ class InstalledAddonDetailsFragment : Fragment() {
switch.setOnCheckedChangeListener { v, isChecked -> switch.setOnCheckedChangeListener { v, isChecked ->
val addonManager = v.context.components.addonManager val addonManager = v.context.components.addonManager
switch.isClickable = false switch.isClickable = false
binding.removeAddOn.isEnabled = false disableButtons()
addonManager.setAddonAllowedInPrivateBrowsing( addonManager.setAddonAllowedInPrivateBrowsing(
addon, addon,
isChecked, isChecked,
@ -312,14 +340,14 @@ class InstalledAddonDetailsFragment : Fragment() {
runIfFragmentIsAttached { runIfFragmentIsAttached {
this.addon = it this.addon = it
switch.isClickable = true switch.isClickable = true
binding.removeAddOn.isEnabled = true enableButtons()
} }
}, },
onError = { onError = {
runIfFragmentIsAttached { runIfFragmentIsAttached {
switch.isChecked = addon.isAllowedInPrivateBrowsing() switch.isChecked = addon.isAllowedInPrivateBrowsing()
switch.isClickable = true switch.isClickable = true
binding.removeAddOn.isEnabled = true enableButtons()
} }
}, },
) )
@ -373,6 +401,17 @@ class InstalledAddonDetailsFragment : Fragment() {
binding.details.isClickable = clickable binding.details.isClickable = clickable
binding.permissions.isClickable = clickable binding.permissions.isClickable = clickable
binding.removeAddOn.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) { 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.ext.settings
import org.mozilla.fenix.home.HomeScreenViewModel import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.home.SharedViewModel import org.mozilla.fenix.home.SharedViewModel
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.biometric.BiometricPromptFeature import org.mozilla.fenix.settings.biometric.BiometricPromptFeature
@ -230,6 +231,7 @@ abstract class BaseBrowserFragment :
internal val sharedViewModel: SharedViewModel by activityViewModels() internal val sharedViewModel: SharedViewModel by activityViewModels()
private val homeViewModel: HomeScreenViewModel by activityViewModels() private val homeViewModel: HomeScreenViewModel by activityViewModels()
private val bookmarksSharedViewModel: BookmarksSharedViewModel by activityViewModels()
private var currentStartDownloadDialog: StartDownloadDialog? = null private var currentStartDownloadDialog: StartDownloadDialog? = null
@ -1414,7 +1416,7 @@ abstract class BaseBrowserFragment :
// Save bookmark, then go to edit fragment // Save bookmark, then go to edit fragment
try { try {
val guid = bookmarksStorage.addItem( val guid = bookmarksStorage.addItem(
BookmarkRoot.Mobile.id, bookmarksSharedViewModel.selectedFolder?.guid ?: BookmarkRoot.Mobile.id,
url = sessionUrl, url = sessionUrl,
title = sessionTitle, title = sessionTitle,
position = null, 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.ktx.android.view.getRectWithViewLocation
import mozilla.components.support.utils.ext.bottom import mozilla.components.support.utils.ext.bottom
import mozilla.components.support.utils.ext.mandatorySystemGestureInsets 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.R
import org.mozilla.fenix.ext.getRectWithScreenLocation import org.mozilla.fenix.ext.getRectWithScreenLocation
import org.mozilla.fenix.ext.getWindowInsets import org.mozilla.fenix.ext.getWindowInsets
@ -260,6 +262,7 @@ class ToolbarGestureHandler(
object : AnimatorListenerAdapter() { object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
tabPreview.isVisible = false 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, R.color.fx_mobile_layer_color_1,
), ),
httpsOnlyMode = context.settings().getHttpsOnlyMode(), httpsOnlyMode = context.settings().getHttpsOnlyMode(),
globalPrivacyControlEnabled = context.settings().shouldEnableGlobalPrivacyControl,
cookieBannerHandlingMode = context.settings().getCookieBannerHandling(), cookieBannerHandlingMode = context.settings().getCookieBannerHandling(),
cookieBannerHandlingModePrivateBrowsing = context.settings().getCookieBannerHandlingPrivateMode(), cookieBannerHandlingModePrivateBrowsing = context.settings().getCookieBannerHandlingPrivateMode(),
cookieBannerHandlingDetectOnlyMode = context.settings().shouldEnableCookieBannerDetectOnly, cookieBannerHandlingDetectOnlyMode = context.settings().shouldEnableCookieBannerDetectOnly,
cookieBannerHandlingGlobalRules = context.settings().shouldEnableCookieBannerGlobalRules, cookieBannerHandlingGlobalRules = context.settings().shouldEnableCookieBannerGlobalRules,
cookieBannerHandlingGlobalRulesSubFrames = context.settings().shouldEnableCookieBannerGlobalRulesSubFrame, cookieBannerHandlingGlobalRulesSubFrames = context.settings().shouldEnableCookieBannerGlobalRulesSubFrame,
emailTrackerBlockingPrivateBrowsing = true,
) )
GeckoEngine( GeckoEngine(

View File

@ -5,6 +5,7 @@
package org.mozilla.fenix.components package org.mozilla.fenix.components
import android.content.Context import android.content.Context
import mozilla.components.concept.base.crash.CrashReporting
import mozilla.components.feature.fxsuggest.FxSuggestIngestionScheduler import mozilla.components.feature.fxsuggest.FxSuggestIngestionScheduler
import mozilla.components.feature.fxsuggest.FxSuggestStorage import mozilla.components.feature.fxsuggest.FxSuggestStorage
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
@ -13,10 +14,12 @@ import org.mozilla.fenix.perf.lazyMonitored
* Component group for Firefox Suggest. * Component group for Firefox Suggest.
* *
* @param context The Android application context. * @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 { val storage by lazyMonitored {
FxSuggestStorage(context) FxSuggestStorage(context, crashReporter)
} }
val ingestionScheduler by lazyMonitored { val ingestionScheduler by lazyMonitored {

View File

@ -11,6 +11,7 @@ import kotlinx.coroutines.launch
import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature
import mozilla.components.feature.app.links.AppLinksInterceptor import mozilla.components.feature.app.links.AppLinksInterceptor
import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.FxaAccountManager
import org.mozilla.fenix.ext.application
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
@ -38,4 +39,10 @@ class Services(
launchInApp = { context.settings().shouldOpenLinksInApp() }, 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.StandardSnackbarError
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.AppStore 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.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
@ -250,5 +251,12 @@ sealed class AppAction : Action {
val productPageUrl: String, val productPageUrl: String,
val expanded: Boolean, val expanded: Boolean,
) : ShoppingAction() ) : 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 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 * @property productCardState Map of product url to [CardState] that contains the state of different
* cards in the shopping sheet. * cards in the shopping sheet.
* @property recordedProductRecommendationImpressions Set of [ProductRecommendationImpressionKey]
* that contains the product recommendation impressions that have been recorded.
*/ */
data class ShoppingState( data class ShoppingState(
val shoppingSheetExpanded: Boolean? = null, val shoppingSheetExpanded: Boolean? = null,
val productCardState: Map<String, CardState> = emptyMap(), 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. * 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 = private fun ShoppingState.updateProductCardState(key: String, value: CardState): ShoppingState =

View File

@ -29,13 +29,18 @@ class BookmarksUseCase(
* one with the identical [url] already exists. * one with the identical [url] already exists.
*/ */
@WorkerThread @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 { return try {
val canAdd = storage.getBookmarksWithUrl(url).firstOrNull { it.url == url } == null val canAdd = storage.getBookmarksWithUrl(url).firstOrNull { it.url == url } == null
if (canAdd) { if (canAdd) {
storage.addItem( storage.addItem(
BookmarkRoot.Mobile.id, parentGuid ?: BookmarkRoot.Mobile.id,
url = url, url = url,
title = title, title = title,
position = position, 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.AMP_SUGGESTION_CLICKED,
Component.FEATURE_FXSUGGEST to FxSuggestFacts.Items.WIKIPEDIA_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.pingType.set("fxsuggest-click")
FxSuggest.isClicked.set(true) FxSuggest.isClicked.set(true)
(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)?.let { (metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)?.let {
FxSuggest.position.set(it) FxSuggest.position.set(it)
} }
when (val clickInfo = metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO)) { when (clickInfo) {
is FxSuggestInteractionInfo.Amp -> { is FxSuggestInteractionInfo.Amp -> {
FxSuggest.blockId.set(clickInfo.blockId) FxSuggest.blockId.set(clickInfo.blockId)
FxSuggest.advertiser.set(clickInfo.advertiser) 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.AMP_SUGGESTION_IMPRESSED,
Component.FEATURE_FXSUGGEST to FxSuggestFacts.Items.WIKIPEDIA_SUGGESTION_IMPRESSED, Component.FEATURE_FXSUGGEST to FxSuggestFacts.Items.WIKIPEDIA_SUGGESTION_IMPRESSED,
-> { -> {
FxSuggest.pingType.set("fxsuggest-impression") val impressionInfo = metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO)
(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)?.let { val engagementAbandoned = metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean
FxSuggest.isClicked.set(it) ?: false
}
(metadata?.get(FxSuggestFacts.MetadataKeys.POSITION) as? Long)?.let { // Record an event for this impression in the `events` ping. These events include the `client_id`, and
FxSuggest.position.set(it) // we record them for engaged and abandoned search sessions.
} when (impressionInfo) {
when (val impressionInfo = metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO)) {
is FxSuggestInteractionInfo.Amp -> { is FxSuggestInteractionInfo.Amp -> {
FxSuggest.blockId.set(impressionInfo.blockId) Awesomebar.sponsoredSuggestionImpressed.record(
FxSuggest.advertiser.set(impressionInfo.advertiser) Awesomebar.SponsoredSuggestionImpressedExtra(
FxSuggest.reportingUrl.set(impressionInfo.reportingUrl) provider = "amp",
FxSuggest.iabCategory.set(impressionInfo.iabCategory) engagementAbandoned = engagementAbandoned,
FxSuggest.contextId.set(UUID.fromString(impressionInfo.contextId)) ),
)
} }
is FxSuggestInteractionInfo.Wikipedia -> { is FxSuggestInteractionInfo.Wikipedia -> {
FxSuggest.advertiser.set("wikipedia") Awesomebar.nonSponsoredSuggestionImpressed.record(
FxSuggest.contextId.set(UUID.fromString(impressionInfo.contextId)) 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 -> { 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.components.toolbar.interactor.BrowserToolbarInteractor
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.HomeScreenViewModel import org.mozilla.fenix.home.HomeScreenViewModel
@ -221,9 +222,9 @@ class DefaultBrowserToolbarController(
} }
override fun handleTranslationsButtonClick() { override fun handleTranslationsButtonClick() {
navController.navigate( val directions =
BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment(), BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment()
) navController.navigateSafe(R.id.browserFragment, directions)
} }
companion object { 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. * bounds defined by the width and height.
* @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be used * @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]. * 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 @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -46,9 +50,11 @@ fun Image(
contentDescription: String? = null, contentDescription: String? = null,
alignment: Alignment = Alignment.Center, alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit, contentScale: ContentScale = ContentScale.Fit,
placeholder: @Composable () -> Unit = { DefaultImagePlaceholder(modifier, contentDescription) },
fallback: @Composable () -> Unit = { DefaultImagePlaceholder(modifier, contentDescription) },
) { ) {
if (inComposePreview) { if (inComposePreview) {
DefaultImagePlaceholder(modifier = modifier) placeholder()
} else { } else {
ImageLoader( ImageLoader(
url = url, 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.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.painter.ColorPainter 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import mozilla.components.support.images.compose.loader.Fallback 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 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 placeholder [Composable] composable used during loading.
* @param contentDescription Text provided to accessibility services to describe what this image represents. * By default, set to [DefaultImagePlaceholder] in [org.mozilla.fenix.compose.Image].
* Defaults to [null] suited for an image used only for decorative purposes and not to be read by
* accessibility services.
*/ */
@Composable @Composable
internal fun ImageLoaderScope.WithDefaultPlaceholder( internal fun ImageLoaderScope.WithPlaceholder(
modifier: Modifier, placeholder: @Composable () -> Unit,
contentDescription: String? = null,
) { ) {
Placeholder { 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 fallback [Painter] composable used if loading failed.
* @param contentDescription Text provided to accessibility services to describe what this image represents. * By default, set to [DefaultImagePlaceholder] in [org.mozilla.fenix.compose.Image].
* Defaults to [null] suited for an image used only for decorative purposes and not to be read by
* accessibility services.
*/ */
@Composable @Composable
internal fun ImageLoaderScope.WithDefaultFallback( internal fun ImageLoaderScope.WithFallback(
modifier: Modifier, fallback: @Composable () -> Unit,
contentDescription: String? = null,
) { ) {
Fallback { Fallback {
DefaultImagePlaceholder(modifier, contentDescription) fallback()
} }
} }
@ -75,7 +70,7 @@ internal fun DefaultImagePlaceholder(
private fun DefaultImagePlaceholderPreview() { private fun DefaultImagePlaceholderPreview() {
FirefoxTheme { FirefoxTheme {
DefaultImagePlaceholder( DefaultImagePlaceholder(
Modifier modifier = Modifier
.size(200.dp, 100.dp) .size(200.dp, 100.dp)
.clip(RoundedCornerShape(8.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.width
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.FloatingActionButton import androidx.compose.material.FloatingActionButton
import androidx.compose.material.FloatingActionButtonDefaults
import androidx.compose.material.FloatingActionButtonElevation
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable 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 modifier [Modifier] to be applied to the action button.
* @param contentDescription The content description to describe the icon. * @param contentDescription The content description to describe the icon.
* @param label Text to be displayed next to 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. * @param onClick Invoked when the button is clicked.
*/ */
@Composable @Composable
@ -43,6 +47,7 @@ fun FloatingActionButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
contentDescription: String? = null, contentDescription: String? = null,
label: String? = null, label: String? = null,
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(defaultElevation = 5.dp),
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
FloatingActionButton( FloatingActionButton(
@ -50,6 +55,7 @@ fun FloatingActionButton(
modifier = modifier, modifier = modifier,
backgroundColor = FirefoxTheme.colors.actionPrimary, backgroundColor = FirefoxTheme.colors.actionPrimary,
contentColor = FirefoxTheme.colors.textActionPrimary, contentColor = FirefoxTheme.colors.textActionPrimary,
elevation = elevation,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier

View File

@ -19,14 +19,23 @@ import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.compose.Favicon 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 import org.mozilla.fenix.theme.FirefoxTheme
private val LIST_ITEM_HEIGHT = 56.dp 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 label The label in the list item.
* @param modifier [Modifier] to be applied to the layout. * @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 description An optional description text below the label.
* @param maxDescriptionLines An optional maximum number of lines for the description text to span. * @param maxDescriptionLines An optional maximum number of lines for the description text to span.
* @param onClick Called when the user clicks on the item. * @param onClick Called when the user clicks on the item.
@ -50,6 +60,7 @@ private val ICON_SIZE = 24.dp
fun TextListItem( fun TextListItem(
label: String, label: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
maxLabelLines: Int = 1,
description: String? = null, description: String? = null,
maxDescriptionLines: Int = 1, maxDescriptionLines: Int = 1,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
@ -59,6 +70,7 @@ fun TextListItem(
) { ) {
ListItem( ListItem(
label = label, label = label,
maxLabelLines = maxLabelLines,
modifier = modifier, modifier = modifier,
description = description, description = description,
maxDescriptionLines = maxDescriptionLines, maxDescriptionLines = maxDescriptionLines,
@ -69,7 +81,8 @@ fun TextListItem(
onClick = onIconClick, onClick = onIconClick,
modifier = Modifier modifier = Modifier
.padding(end = 16.dp) .padding(end = 16.dp)
.size(ICON_SIZE), .size(ICON_SIZE)
.clearAndSetSemantics {},
) { ) {
Icon( Icon(
painter = iconPainter, 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 * 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. * the flexibility to add custom UI to either end of the item.
* *
* @param label The label in the list item. * @param label The label in the list item.
* @param modifier [Modifier] to be applied to the layout. * @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 description An optional description text below the label.
* @param maxDescriptionLines An optional maximum number of lines for the description text to span. * @param maxDescriptionLines An optional maximum number of lines for the description text to span.
* @param onClick Called when the user clicks on the item. * @param onClick Called when the user clicks on the item.
@ -216,6 +281,7 @@ fun IconListItem(
private fun ListItem( private fun ListItem(
label: String, label: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
maxLabelLines: Int = 1,
description: String? = null, description: String? = null,
maxDescriptionLines: Int = 1, maxDescriptionLines: Int = 1,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
@ -242,7 +308,7 @@ private fun ListItem(
text = label, text = label,
color = FirefoxTheme.colors.textPrimary, color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.subtitle1, style = FirefoxTheme.typography.subtitle1,
maxLines = 1, maxLines = maxLabelLines,
) )
description?.let { 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 { (binding.root.layoutParams as CoordinatorLayout.LayoutParams).apply {
(behavior as DynamicDownloadDialogBehavior).forceExpand(binding.root) (behavior as DynamicDownloadDialogBehavior).forceExpand(binding.root)
} }
if(!settings.shouldShowSuccessDownloadDialog && !didFail) {
dismiss()
}
} }
private fun dismiss() { private fun dismiss() {

View File

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

View File

@ -76,6 +76,18 @@ class HomeMenuView(
ThemeManager.resolveAttribute(R.attr.textPrimary, context), 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()) { if (settings.showTopSitesFeature && topSites.isNotEmpty()) {
shouldShowCustomizeHome = true
if (settings.enableComposeTopSites) { if (settings.enableComposeTopSites) {
items.add(AdapterItem.TopSites) items.add(AdapterItem.TopSites)
} else { } else {
@ -103,7 +104,7 @@ internal fun normalModeAdapterItems(
} }
if (shouldShowCustomizeHome) { if (shouldShowCustomizeHome) {
items.add(AdapterItem.CustomizeHomeButton) /* noop */
} }
items.add(AdapterItem.BottomSpacer) items.add(AdapterItem.BottomSpacer)

View File

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

View File

@ -36,7 +36,7 @@ class BookmarkItemMenu(
@VisibleForTesting @VisibleForTesting
@SuppressWarnings("LongMethod") @SuppressWarnings("LongMethod")
internal suspend fun menuItems(itemType: BookmarkNodeType, itemId: String): List<TextMenuCandidate> { 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( return listOfNotNull(
if (itemType != BookmarkNodeType.SEPARATOR) { 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? { private suspend fun virtualDesktopFolder(): BookmarkNode? {
val rootNode = bookmarksStorage.getTree(BookmarkRoot.Root.id, recursive = false) ?: return null val rootNode = bookmarksStorage.getTree(BookmarkRoot.Root.id, recursive = false) ?: return null
return rootNode.copy(title = rootTitles[rootNode.title]) return rootNode.copy(title = rootTitles[rootNode.title])

View File

@ -35,11 +35,14 @@ object CustomAttributeProvider : JexlAttributeProvider {
* will unlikely to targeted as expected. * will unlikely to targeted as expected.
*/ */
fun getCustomTargetingAttributes(context: Context): JSONObject { fun getCustomTargetingAttributes(context: Context): JSONObject {
val isFirstRun = context.settings().isFirstNimbusRun val settings = context.settings()
val isFirstRun = settings.isFirstNimbusRun
val isReviewCheckerEnabled = settings.isReviewQualityCheckEnabled
return JSONObject( return JSONObject(
mapOf( mapOf(
// By convention, we should use snake case. // By convention, we should use snake case.
"is_first_run" to isFirstRun, "is_first_run" to isFirstRun,
"is_review_checker_enabled" to isReviewCheckerEnabled,
// This camelCase attribute is a boolean value represented as a string. // This camelCase attribute is a boolean value represented as a string.
// This is left for backwards compatibility. // This is left for backwards compatibility.
@ -74,7 +77,8 @@ object CustomAttributeProvider : JexlAttributeProvider {
UTM_TERM to settings.utmTerm, UTM_TERM to settings.utmTerm,
UTM_CONTENT to settings.utmContent, 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.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import mozilla.components.service.nimbus.messaging.FxNimbusMessaging import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
import mozilla.components.service.nimbus.messaging.Message import mozilla.components.service.nimbus.messaging.Message
@ -32,21 +32,21 @@ const val CLICKED_MESSAGE_ID = "clickedMessageId"
const val DISMISSED_MESSAGE_ID = "dismissedMessageId" 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] * A [Notification] will be created using the configuration of the next highest priority [Message]
* if it has not already been displayed. * if it has not already been displayed.
*/ */
class MessageNotificationWorker( class MessageNotificationWorker(
context: Context, context: Context,
workerParameters: WorkerParameters, workerParameters: WorkerParameters,
) : Worker(context, workerParameters) { ) : CoroutineWorker(context, workerParameters) {
@SuppressWarnings("ReturnCount") @SuppressWarnings("ReturnCount")
override fun doWork(): Result { override suspend fun doWork(): Result {
val context = applicationContext val context = applicationContext
val messagingStorage = context.components.analytics.messagingStorage val messagingStorage = context.components.analytics.messagingStorage
val messages = runBlockingIncrement { messagingStorage.getMessages() } val messages = messagingStorage.getMessages()
val nextMessage = val nextMessage =
messagingStorage.getNextMessage(FenixMessageSurfaceId.NOTIFICATION, messages) messagingStorage.getNextMessage(FenixMessageSurfaceId.NOTIFICATION, messages)
?: return Result.success() ?: return Result.success()
@ -67,7 +67,7 @@ class MessageNotificationWorker(
currentBootUniqueIdentifier, currentBootUniqueIdentifier,
) )
runBlockingIncrement { nimbusMessagingController.onMessageDisplayed(updatedMessage) } nimbusMessagingController.onMessageDisplayed(updatedMessage)
context.components.notificationsDelegate.notify( context.components.notificationsDelegate.notify(
MESSAGE_TAG, MESSAGE_TAG,
@ -137,7 +137,7 @@ class MessageNotificationWorker(
private const val MESSAGE_WORK_NAME = "org.mozilla.fenix.message.work" 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) { fun setMessageNotificationWorker(context: Context) {
val messaging = FxNimbusMessaging.features.messaging 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.openSetDefaultBrowserOption
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.nimbus.FxNimbus 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.OnboardingPageUiData
import org.mozilla.fenix.onboarding.view.OnboardingScreen
import org.mozilla.fenix.onboarding.view.sequencePosition import org.mozilla.fenix.onboarding.view.sequencePosition
import org.mozilla.fenix.onboarding.view.telemetrySequenceId import org.mozilla.fenix.onboarding.view.telemetrySequenceId
import org.mozilla.fenix.onboarding.view.toPageUiData import org.mozilla.fenix.onboarding.view.toPageUiData
@ -43,9 +43,9 @@ import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.gecko.search.SearchWidgetProvider 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 { private val pagesToDisplay by lazy {
pagesToDisplay( pagesToDisplay(
@ -53,7 +53,7 @@ class JunoOnboardingFragment : Fragment() {
canShowAddWidgetCard(), canShowAddWidgetCard(),
) )
} }
private val telemetryRecorder by lazy { JunoOnboardingTelemetryRecorder() } private val telemetryRecorder by lazy { OnboardingTelemetryRecorder() }
private val pinAppWidgetReceiver = WidgetPinnedReceiver() private val pinAppWidgetReceiver = WidgetPinnedReceiver()
@SuppressLint("SourceLockedOrientationActivity") @SuppressLint("SourceLockedOrientationActivity")
@ -98,7 +98,7 @@ class JunoOnboardingFragment : Fragment() {
@Suppress("LongMethod") @Suppress("LongMethod")
private fun ScreenContent() { private fun ScreenContent() {
val context = LocalContext.current val context = LocalContext.current
JunoOnboardingScreen( OnboardingScreen(
pagesToDisplay = pagesToDisplay, pagesToDisplay = pagesToDisplay,
onMakeFirefoxDefaultClick = { onMakeFirefoxDefaultClick = {
activity?.openSetDefaultBrowserOption(useCustomTab = true) activity?.openSetDefaultBrowserOption(useCustomTab = true)
@ -127,8 +127,8 @@ class JunoOnboardingFragment : Fragment() {
}, },
onSignInButtonClick = { onSignInButtonClick = {
findNavController().nav( findNavController().nav(
id = R.id.junoOnboardingFragment, id = R.id.onboardingFragment,
directions = JunoOnboardingFragmentDirections.actionGlobalTurnOnSync( directions = OnboardingFragmentDirections.actionGlobalTurnOnSync(
entrypoint = FenixFxAEntryPoint.NewUserOnboarding, entrypoint = FenixFxAEntryPoint.NewUserOnboarding,
), ),
) )
@ -203,8 +203,8 @@ class JunoOnboardingFragment : Fragment() {
private fun onFinish(sequenceId: String, sequencePosition: String) { private fun onFinish(sequenceId: String, sequencePosition: String) {
requireComponents.fenixOnboarding.finish() requireComponents.fenixOnboarding.finish()
findNavController().nav( findNavController().nav(
id = R.id.junoOnboardingFragment, id = R.id.onboardingFragment,
directions = JunoOnboardingFragmentDirections.actionHome(), directions = OnboardingFragmentDirections.actionHome(),
) )
telemetryRecorder.onOnboardingComplete( telemetryRecorder.onOnboardingComplete(
sequenceId = sequenceId, sequenceId = sequenceId,
@ -224,8 +224,7 @@ class JunoOnboardingFragment : Fragment() {
showNotificationPage: Boolean, showNotificationPage: Boolean,
showAddWidgetPage: Boolean, showAddWidgetPage: Boolean,
): List<OnboardingPageUiData> { ): List<OnboardingPageUiData> {
val junoOnboardingFeature = FxNimbus.features.junoOnboarding.value() val jexlConditions = FxNimbus.features.junoOnboarding.value().conditions
val jexlConditions = junoOnboardingFeature.conditions
val jexlHelper = requireContext().components.analytics.messagingStorage.helper val jexlHelper = requireContext().components.analytics.messagingStorage.helper
return FxNimbus.features.junoOnboarding.value().cards.values.toPageUiData( 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 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. * 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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import mozilla.components.support.utils.PendingIntentUtils 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 * 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() { class WidgetPinnedReceiver : BroadcastReceiver() {
@ -44,7 +44,7 @@ class WidgetPinnedReceiver : BroadcastReceiver() {
/** /**
* Object containing boolean that updates behavior of Add Search Widget * 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. * - True if widget added successfully and app resumed from launcher add widget dialog.
* - False if dialog opened but widget was not added. * - 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 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 pagesToDisplay List of pages to be displayed in onboarding pager ui.
* @param onMakeFirefoxDefaultClick Invoked when positive button on default browser page is clicked. * @param onMakeFirefoxDefaultClick Invoked when positive button on default browser page is clicked.
@ -61,7 +61,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
*/ */
@Composable @Composable
@Suppress("LongParameterList", "LongMethod") @Suppress("LongParameterList", "LongMethod")
fun JunoOnboardingScreen( fun OnboardingScreen(
pagesToDisplay: List<OnboardingPageUiData>, pagesToDisplay: List<OnboardingPageUiData>,
onMakeFirefoxDefaultClick: () -> Unit, onMakeFirefoxDefaultClick: () -> Unit,
onSkipDefaultClick: () -> Unit, onSkipDefaultClick: () -> Unit,
@ -116,7 +116,7 @@ fun JunoOnboardingScreen(
} }
} }
JunoOnboardingContent( OnboardingContent(
pagesToDisplay = pagesToDisplay, pagesToDisplay = pagesToDisplay,
pagerState = pagerState, pagerState = pagerState,
onMakeFirefoxDefaultClick = { onMakeFirefoxDefaultClick = {
@ -162,7 +162,7 @@ fun JunoOnboardingScreen(
@Composable @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
private fun JunoOnboardingContent( private fun OnboardingContent(
pagesToDisplay: List<OnboardingPageUiData>, pagesToDisplay: List<OnboardingPageUiData>,
pagerState: PagerState, pagerState: PagerState,
onMakeFirefoxDefaultClick: () -> Unit, onMakeFirefoxDefaultClick: () -> Unit,
@ -241,10 +241,10 @@ private class DisableForwardSwipeNestedScrollConnection(
@LightDarkPreview @LightDarkPreview
@Composable @Composable
private fun JunoOnboardingScreenPreview() { private fun OnboardingScreenPreview() {
val pageCount = defaultPreviewPages().size val pageCount = defaultPreviewPages().size
FirefoxTheme { FirefoxTheme {
JunoOnboardingContent( OnboardingContent(
pagesToDisplay = defaultPreviewPages(), pagesToDisplay = defaultPreviewPages(),
pagerState = rememberPagerState(initialPage = 0) { pagerState = rememberPagerState(initialPage = 0) {
pageCount pageCount

View File

@ -28,11 +28,9 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.Core import org.mozilla.fenix.components.Core
import org.mozilla.fenix.components.metrics.MetricsUtils import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.crashes.CrashListActivity import org.mozilla.fenix.crashes.CrashListActivity
import org.mozilla.fenix.ext.application
import org.mozilla.fenix.ext.navigateSafe import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.telemetryName 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.SearchSelectorInteractor
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
@ -114,12 +112,6 @@ class SearchDialogController(
val searchEngine = fragmentStore.state.searchEngineSource.searchEngine val searchEngine = fragmentStore.state.searchEngineSource.searchEngine
val isDefaultEngine = searchEngine == fragmentStore.state.defaultEngine 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( activity.openToBrowserAndLoad(
searchTermOrURL = url, searchTermOrURL = url,
@ -127,9 +119,7 @@ class SearchDialogController(
from = BrowserDirection.FromSearchDialog, from = BrowserDirection.FromSearchDialog,
engine = searchEngine, engine = searchEngine,
forceSearch = !isDefaultEngine, forceSearch = !isDefaultEngine,
flags = flags,
requestDesktopMode = fromHomeScreen && activity.settings().openNextTabInDesktopMode, requestDesktopMode = fromHomeScreen && activity.settings().openNextTabInDesktopMode,
additionalHeaders = additionalHeaders,
) )
if (url.isUrl() || searchEngine == null) { if (url.isUrl() || searchEngine == null) {
@ -195,12 +185,6 @@ class SearchDialogController(
clearToolbarFocus() clearToolbarFocus()
val searchEngine = fragmentStore.state.searchEngineSource.searchEngine 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( activity.openToBrowserAndLoad(
searchTermOrURL = searchTerms, searchTermOrURL = searchTerms,
@ -208,8 +192,6 @@ class SearchDialogController(
from = BrowserDirection.FromSearchDialog, from = BrowserDirection.FromSearchDialog,
engine = searchEngine, engine = searchEngine,
forceSearch = true, forceSearch = true,
flags = flags,
additionalHeaders = additionalHeaders,
) )
val searchAccessPoint = when (fragmentStore.state.searchAccessPoint) { val searchAccessPoint = when (fragmentStore.state.searchAccessPoint) {
@ -344,20 +326,4 @@ class SearchDialogController(
create().withCenterAlignedButtons() 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 mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Awesomebar import org.mozilla.fenix.GleanMetrics.Awesomebar
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.VoiceSearch import org.mozilla.fenix.GleanMetrics.VoiceSearch
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -863,6 +864,8 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
return return
} }
Events.browserToolbarQrScanTapped.record(NoExtras())
view?.hideKeyboard() view?.hideKeyboard()
toolbarView.view.clearFocus() toolbarView.view.clearFocus()

View File

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

View File

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

View File

@ -54,6 +54,7 @@ class CustomizationFragment : PreferenceFragmentCompat() {
setupRadioGroups() setupRadioGroups()
setupToolbarCategory() setupToolbarCategory()
setupGesturesCategory() setupGesturesCategory()
setupDownloadCustomizationCategory()
setupAddonsCustomizationCategory() setupAddonsCustomizationCategory()
setupSystemBehaviorCategory() setupSystemBehaviorCategory()
requirePreference<SwitchPreference>(R.string.pref_key_strip_url).apply { 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 { override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) { when (preference.key) {
resources.getString(R.string.pref_key_website_pull_to_refresh) -> { 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 android.view.ViewGroup
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment 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.R
import org.mozilla.fenix.components.components import org.mozilla.fenix.components.components
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
@ -39,7 +41,7 @@ class SecretDebugSettingsFragment : Fragment() {
return ComposeView(requireContext()).apply { return ComposeView(requireContext()).apply {
setContent { setContent {
FirefoxTheme { FirefoxTheme {
DebugInfo() SecretDebugSettingsScreen()
} }
} }
} }
@ -47,33 +49,41 @@ class SecretDebugSettingsFragment : Fragment() {
} }
@Composable @Composable
private fun DebugInfo() { private fun SecretDebugSettingsScreen() {
val store = components.core.store 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( Column(
modifier = Modifier modifier = Modifier
.padding(8.dp), .padding(8.dp),
) { ) {
Text( Text(
text = stringResource(R.string.debug_info_region_home), text = stringResource(R.string.debug_info_region_home),
style = MaterialTheme.typography.h6, color = FirefoxTheme.colors.textPrimary,
color = MaterialTheme.colors.onBackground, style = FirefoxTheme.typography.headline6,
modifier = Modifier.padding(4.dp), modifier = Modifier.padding(4.dp),
) )
Text( Text(
text = store.state.search.region?.home ?: "Unknown", text = regionState.home,
color = MaterialTheme.colors.onBackground, color = FirefoxTheme.colors.textPrimary,
modifier = Modifier.padding(4.dp), modifier = Modifier.padding(4.dp),
) )
Text( Text(
text = stringResource(R.string.debug_info_region_current), text = stringResource(R.string.debug_info_region_current),
style = MaterialTheme.typography.h6, color = FirefoxTheme.colors.textPrimary,
color = MaterialTheme.colors.onBackground, style = FirefoxTheme.typography.headline6,
modifier = Modifier.padding(4.dp), modifier = Modifier.padding(4.dp),
) )
Text( Text(
text = store.state.search.region?.current ?: "Unknown", text = regionState.current,
color = MaterialTheme.colors.onBackground, color = FirefoxTheme.colors.textPrimary,
modifier = Modifier.padding(4.dp), modifier = Modifier.padding(4.dp),
) )
} }

View File

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

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