v122
parent
ecd7c8bc60
commit
8aff189c63
|
@ -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
|
|
@ -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'
|
||||||
]))
|
]))
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,52 @@
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
---
|
||||||
|
# This file configures "evergreen" messages that are displayed via
|
||||||
|
# the Nimbus Messaging system.
|
||||||
|
#
|
||||||
|
# They are "evergreen" in that they apply to all users, and shipped with the app.
|
||||||
|
#
|
||||||
|
# This file is intended to grow new messages once messages have been tested via
|
||||||
|
# experiment, rolled out to everyone in the release, and are ready to be rolled out
|
||||||
|
# without the remote prompting from Experimenter.
|
||||||
|
#
|
||||||
|
# When adding new messages to this file, please add the experiment (and/or rollout) URLs used to
|
||||||
|
# validate them.
|
||||||
|
#
|
||||||
|
# Triggers, actions and styles are configured in messaging-fenix.fml.yaml.
|
||||||
|
import:
|
||||||
|
- path: ../android-components/components/service/nimbus/messaging.fml.yaml
|
||||||
|
channel: release
|
||||||
|
features:
|
||||||
|
messaging:
|
||||||
|
# This message displays on the homescreen, asking the user to set Firefox as the default.
|
||||||
|
# It is triggered after a minimum of 4 launches of the app.
|
||||||
|
- value:
|
||||||
|
messages:
|
||||||
|
default-browser:
|
||||||
|
text: default_browser_experiment_card_text
|
||||||
|
surface: homescreen
|
||||||
|
action: "MAKE_DEFAULT_BROWSER"
|
||||||
|
trigger:
|
||||||
|
- I_AM_NOT_DEFAULT_BROWSER
|
||||||
|
- USER_ESTABLISHED_INSTALL
|
||||||
|
style: PERSISTENT
|
||||||
|
button-label: preferences_set_as_default_browser
|
||||||
|
|
||||||
|
triggers:
|
||||||
|
USER_ESTABLISHED_INSTALL: "number_of_app_launches >=4"
|
||||||
|
|
||||||
|
# This message displays as a 'push' notification, asking the user to set Firefox as the default.
|
||||||
|
# It is triggered three days after install.
|
||||||
|
- value:
|
||||||
|
messages:
|
||||||
|
default-browser-notification:
|
||||||
|
title: nimbus_notification_default_browser_title
|
||||||
|
text: nimbus_notification_default_browser_text
|
||||||
|
surface: notification
|
||||||
|
style: NOTIFICATION
|
||||||
|
trigger:
|
||||||
|
- I_AM_NOT_DEFAULT_BROWSER
|
||||||
|
- DAY_3_AFTER_INSTALL
|
||||||
|
action: MAKE_DEFAULT_BROWSER
|
|
@ -0,0 +1,112 @@
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
---
|
||||||
|
includes:
|
||||||
|
- messaging-evergreen-messages.fml.yaml
|
||||||
|
import:
|
||||||
|
- path: ../android-components/components/service/nimbus/messaging.fml.yaml
|
||||||
|
channel: release
|
||||||
|
features:
|
||||||
|
messaging:
|
||||||
|
- value:
|
||||||
|
triggers:
|
||||||
|
# Using attributes built into the Nimbus SDK
|
||||||
|
USER_RECENTLY_INSTALLED: days_since_install < 7
|
||||||
|
USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update
|
||||||
|
USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale)
|
||||||
|
USER_EN_SPEAKER: "'en' in locale"
|
||||||
|
USER_ES_SPEAKER: "'es' in locale"
|
||||||
|
USER_DE_SPEAKER: "'de' in locale"
|
||||||
|
USER_FR_SPEAKER: "'fr' in locale"
|
||||||
|
DEVICE_ANDROID: os == 'Android'
|
||||||
|
DEVICE_IOS: os == 'iOS'
|
||||||
|
ALWAYS: "true"
|
||||||
|
NEVER: "false"
|
||||||
|
DAY_1_AFTER_INSTALL: days_since_install == 1
|
||||||
|
DAY_2_AFTER_INSTALL: days_since_install == 2
|
||||||
|
DAY_3_AFTER_INSTALL: days_since_install == 3
|
||||||
|
DAY_4_AFTER_INSTALL: days_since_install == 4
|
||||||
|
DAY_5_AFTER_INSTALL: days_since_install == 5
|
||||||
|
MORE_THAN_24H_SINCE_INSTALLED_OR_UPDATED: days_since_update >= 1
|
||||||
|
|
||||||
|
# Using custom attributes for the browser
|
||||||
|
I_AM_DEFAULT_BROWSER: "is_default_browser"
|
||||||
|
I_AM_NOT_DEFAULT_BROWSER: "is_default_browser == false"
|
||||||
|
|
||||||
|
FUNNEL_PAID: "adjust_campaign != ''"
|
||||||
|
FUNNEL_ORGANIC: "adjust_campaign == ''"
|
||||||
|
|
||||||
|
# Using Glean events, specific to the browser
|
||||||
|
INACTIVE_1_DAY: "'app_launched'|eventLastSeen('Hours') >= 24"
|
||||||
|
INACTIVE_2_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 2"
|
||||||
|
INACTIVE_3_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 3"
|
||||||
|
INACTIVE_4_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 4"
|
||||||
|
INACTIVE_5_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 5"
|
||||||
|
|
||||||
|
# Has the user signed in the last 4 years
|
||||||
|
FXA_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) <= 4"
|
||||||
|
FXA_NOT_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) > 4"
|
||||||
|
|
||||||
|
# https://mozilla-hub.atlassian.net/wiki/spaces/FJT/pages/11469471/Core+Active
|
||||||
|
USER_INFREQUENT: "'app_launched'|eventCountNonZero('Days', 28) >= 1 && 'app_launched'|eventCountNonZero('Days', 28) < 7"
|
||||||
|
USER_CASUAL: "'app_launched'|eventCountNonZero('Days', 28) >= 7 && 'app_launched'|eventCountNonZero('Days', 28) < 14"
|
||||||
|
USER_REGULAR: "'app_launched'|eventCountNonZero('Days', 28) >= 14 && 'app_launched'|eventCountNonZero('Days', 28) < 21"
|
||||||
|
USER_CORE_ACTIVE: "'app_launched'|eventCountNonZero('Days', 28) >= 21"
|
||||||
|
|
||||||
|
LAUNCHED_ONCE_THIS_WEEK: "'app_launched'|eventSum('Days', 7) == 1"
|
||||||
|
|
||||||
|
actions:
|
||||||
|
ENABLE_PRIVATE_BROWSING: ://enable_private_browsing
|
||||||
|
INSTALL_SEARCH_WIDGET: ://install_search_widget
|
||||||
|
MAKE_DEFAULT_BROWSER: ://make_default_browser
|
||||||
|
VIEW_BOOKMARKS: ://urls_bookmarks
|
||||||
|
VIEW_COLLECTIONS: ://home_collections
|
||||||
|
VIEW_HISTORY: ://urls_history
|
||||||
|
VIEW_HOMESCREEN: ://home
|
||||||
|
OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility
|
||||||
|
OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager
|
||||||
|
OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data
|
||||||
|
OPEN_SETTINGS_LOGINS: ://settings_logins
|
||||||
|
OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications
|
||||||
|
OPEN_SETTINGS_PRIVACY: ://settings_privacy
|
||||||
|
OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine
|
||||||
|
OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection
|
||||||
|
OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers
|
||||||
|
OPEN_SETTINGS: ://settings
|
||||||
|
TURN_ON_SYNC: ://turn_on_sync
|
||||||
|
styles:
|
||||||
|
DEFAULT:
|
||||||
|
priority: 50
|
||||||
|
max-display-count: 5
|
||||||
|
SURVEY:
|
||||||
|
priority: 55
|
||||||
|
max-display-count: 1
|
||||||
|
PERSISTENT:
|
||||||
|
priority: 50
|
||||||
|
max-display-count: 20
|
||||||
|
WARNING:
|
||||||
|
priority: 60
|
||||||
|
max-display-count: 10
|
||||||
|
URGENT:
|
||||||
|
priority: 100
|
||||||
|
max-display-count: 10
|
||||||
|
NOTIFICATION:
|
||||||
|
priority: 50
|
||||||
|
max-display-count: 1
|
||||||
|
$$surfaces:
|
||||||
|
- homescreen
|
||||||
|
- notification
|
||||||
|
- survey
|
||||||
|
|
||||||
|
- channel: developer
|
||||||
|
value:
|
||||||
|
styles:
|
||||||
|
DEFAULT:
|
||||||
|
priority: 50
|
||||||
|
max-display-count: 100
|
||||||
|
EXPIRES_QUICKLY:
|
||||||
|
priority: 100
|
||||||
|
max-display-count: 1
|
||||||
|
notification-config:
|
||||||
|
refresh-interval: 120 # minutes (2 hours)
|
199
app/metrics.yaml
199
app/metrics.yaml
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<body>
|
||||||
|
<script type = "text/javascript" >
|
||||||
|
const gpcValue = navigator.globalPrivacyControl
|
||||||
|
if (gpcValue) {
|
||||||
|
document.write('<p>GPC is enabled.</p>');
|
||||||
|
} else {
|
||||||
|
document.write('<p>GPC not enabled.</p>');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,23 @@
|
||||||
|
From: https://raw.githubusercontent.com/fonttools/fonttools/main/LICENSE
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Just van Rossum
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,79 @@
|
||||||
|
package org.mozilla.fenix.components
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.components.metrics.fonts.FontEnumerationWorker
|
||||||
|
import org.mozilla.fenix.components.metrics.fonts.FontParser
|
||||||
|
|
||||||
|
class FontParserTest {
|
||||||
|
@Test
|
||||||
|
fun testSanityAssertion() {
|
||||||
|
/*
|
||||||
|
Changing the below constant causes _all_ Nightly users to send a (large) Telemetry event containing
|
||||||
|
their font information. Do not change this value unless you explicitly intend this.
|
||||||
|
*/
|
||||||
|
assertEquals(4, FontEnumerationWorker.kDesiredSubmissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFontParsing() {
|
||||||
|
val assetManager = InstrumentationRegistry.getInstrumentation().context.assets
|
||||||
|
val font1 = FontParser.parse("no-path", assetManager.open("resources/TestTTF.ttf"))
|
||||||
|
assertEquals(
|
||||||
|
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T" +
|
||||||
|
"\u0000T\u0000F",
|
||||||
|
font1.family,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"\u0000V\u0000e\u0000r\u0000s\u0000i\u0000o\u0000n\u0000 \u00001\u0000." +
|
||||||
|
"\u00000\u00000\u00000",
|
||||||
|
font1.fontVersion,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F",
|
||||||
|
font1.fullName,
|
||||||
|
)
|
||||||
|
assertEquals("\u0000R\u0000e\u0000g\u0000u\u0000l\u0000a\u0000r", font1.subFamily)
|
||||||
|
assertEquals(
|
||||||
|
"\u0000F\u0000o\u0000n\u0000t\u0000T\u0000o\u0000o\u0000l\u0000s\u0000:\u0000 " +
|
||||||
|
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F\u0000:\u0000 \u00002\u00000\u00001\u00005",
|
||||||
|
font1.uniqueSubFamily,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"C4E8CE309F44A131D061D73B2580E922A7F5ECC8D7109797AC0FF58BF8723B7B",
|
||||||
|
font1.hash,
|
||||||
|
)
|
||||||
|
assertEquals(3516272951, font1.created)
|
||||||
|
assertEquals(3573411749, font1.modified)
|
||||||
|
assertEquals(65536, font1.revision)
|
||||||
|
val font2 = FontParser.parse("no-path", assetManager.open("resources/TestTTC.ttc"))
|
||||||
|
assertEquals(
|
||||||
|
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T" +
|
||||||
|
"\u0000T\u0000F",
|
||||||
|
font2.family,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"\u0000V\u0000e\u0000r\u0000s\u0000i\u0000o\u0000n\u0000 \u00001\u0000." +
|
||||||
|
"\u00000\u00000\u00000",
|
||||||
|
font2.fontVersion,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F",
|
||||||
|
font2.fullName,
|
||||||
|
)
|
||||||
|
assertEquals("\u0000R\u0000e\u0000g\u0000u\u0000l\u0000a\u0000r", font1.subFamily)
|
||||||
|
assertEquals(
|
||||||
|
"\u0000F\u0000o\u0000n\u0000t\u0000T\u0000o\u0000o\u0000l\u0000s\u0000:\u0000 " +
|
||||||
|
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F\u0000:\u0000 \u00002\u00000\u00001\u00005",
|
||||||
|
font2.uniqueSubFamily,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"A8521588045ED5F1F8B07EECAAC06ED3186C644655BFAC00DD4507CD316FBDC5",
|
||||||
|
font2.hash,
|
||||||
|
)
|
||||||
|
assertEquals(3516272951, font2.created)
|
||||||
|
assertEquals(3573411749, font2.modified)
|
||||||
|
assertEquals(65536, font2.revision)
|
||||||
|
}
|
||||||
|
}
|
|
@ -67,7 +67,10 @@ 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)
|
||||||
|
features = next(iter(branches.get("features")), None)
|
||||||
|
if features.get("messages"):
|
||||||
|
for item in features["value"]["messages"].values():
|
||||||
item["surface"] = "homescreen"
|
item["surface"] = "homescreen"
|
||||||
item["style"] = "URGENT"
|
item["style"] = "URGENT"
|
||||||
for count, trigger in enumerate(item["trigger"]):
|
for count, trigger in enumerate(item["trigger"]):
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from pathlib import Path
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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, "", "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 =
|
|
@ -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": [
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -225,6 +225,10 @@ class ComposeSettingsDeleteBrowsingDataTest {
|
||||||
selectOnlyCookiesCheckBox()
|
selectOnlyCookiesCheckBox()
|
||||||
clickDeleteBrowsingDataButton()
|
clickDeleteBrowsingDataButton()
|
||||||
verifyDeleteBrowsingDataDialog()
|
verifyDeleteBrowsingDataDialog()
|
||||||
|
clickDialogCancelButton()
|
||||||
|
verifyCookiesCheckBox(status = true)
|
||||||
|
clickDeleteBrowsingDataButton()
|
||||||
|
verifyDeleteBrowsingDataDialog()
|
||||||
confirmDeletionAndAssertSnackbar()
|
confirmDeletionAndAssertSnackbar()
|
||||||
exitMenu()
|
exitMenu()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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)")
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.ui
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.customannotations.SmokeTest
|
||||||
|
import org.mozilla.fenix.ext.settings
|
||||||
|
import org.mozilla.fenix.helpers.AppAndSystemHelper
|
||||||
|
import org.mozilla.fenix.helpers.HomeActivityTestRule
|
||||||
|
import org.mozilla.fenix.helpers.TestHelper
|
||||||
|
import org.mozilla.fenix.ui.robots.navigationToolbar
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for verifying the Firefox suggest search fragment
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
class FirefoxSuggestTest {
|
||||||
|
@get:Rule
|
||||||
|
val activityTestRule = AndroidComposeTestRule(
|
||||||
|
HomeActivityTestRule(
|
||||||
|
skipOnboarding = true,
|
||||||
|
isPocketEnabled = false,
|
||||||
|
isJumpBackInCFREnabled = false,
|
||||||
|
isRecentTabsFeatureEnabled = false,
|
||||||
|
isTCPCFREnabled = false,
|
||||||
|
isWallpaperOnboardingEnabled = false,
|
||||||
|
tabsTrayRewriteEnabled = false,
|
||||||
|
),
|
||||||
|
) { it.activity }
|
||||||
|
|
||||||
|
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348361
|
||||||
|
@SmokeTest
|
||||||
|
@Test
|
||||||
|
fun verifyFirefoxSuggestSponsoredSearchResultsTest() {
|
||||||
|
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||||
|
navigationToolbar {
|
||||||
|
}.clickUrlbar {
|
||||||
|
typeSearch(searchTerm = "Amazon")
|
||||||
|
verifySearchEngineSuggestionResults(
|
||||||
|
rule = activityTestRule,
|
||||||
|
searchSuggestions = arrayOf(
|
||||||
|
"Firefox Suggest",
|
||||||
|
"Amazon.com - Official Site",
|
||||||
|
"Sponsored",
|
||||||
|
),
|
||||||
|
searchTerm = "Amazon",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348362
|
||||||
|
@Test
|
||||||
|
fun verifyFirefoxSuggestSponsoredSearchResultsWithPartialKeywordTest() {
|
||||||
|
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||||
|
navigationToolbar {
|
||||||
|
}.clickUrlbar {
|
||||||
|
typeSearch(searchTerm = "Amaz")
|
||||||
|
verifySearchEngineSuggestionResults(
|
||||||
|
rule = activityTestRule,
|
||||||
|
searchSuggestions = arrayOf(
|
||||||
|
"Firefox Suggest",
|
||||||
|
"Amazon.com - Official Site",
|
||||||
|
"Sponsored",
|
||||||
|
),
|
||||||
|
searchTerm = "Amaz",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348363
|
||||||
|
@Test
|
||||||
|
fun openFirefoxSuggestSponsoredSearchResultsTest() {
|
||||||
|
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||||
|
navigationToolbar {
|
||||||
|
}.clickUrlbar {
|
||||||
|
typeSearch(searchTerm = "Amazon")
|
||||||
|
verifySearchEngineSuggestionResults(
|
||||||
|
rule = activityTestRule,
|
||||||
|
searchSuggestions = arrayOf(
|
||||||
|
"Firefox Suggest",
|
||||||
|
"Amazon.com - Official Site",
|
||||||
|
"Sponsored",
|
||||||
|
),
|
||||||
|
searchTerm = "Amazon",
|
||||||
|
)
|
||||||
|
}.clickSearchSuggestion("Amazon.com - Official Site") {
|
||||||
|
waitForPageToLoad()
|
||||||
|
verifyUrl(
|
||||||
|
"amazon.com/?tag=admarketus-20&ref=pd_sl_924ab4435c5a5c23aa2804307ee0669ab36f88caee841ce51d1f2ecb&mfadid=adm",
|
||||||
|
)
|
||||||
|
verifyTabCounter("1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348369
|
||||||
|
@Test
|
||||||
|
fun verifyFirefoxSuggestSponsoredSearchResultsWithEditedKeywordTest() {
|
||||||
|
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||||
|
navigationToolbar {
|
||||||
|
}.clickUrlbar {
|
||||||
|
typeSearch(searchTerm = "Amazon")
|
||||||
|
deleteSearchKeywordCharacters(numberOfDeletionSteps = 3)
|
||||||
|
verifySearchEngineSuggestionResults(
|
||||||
|
rule = activityTestRule,
|
||||||
|
searchSuggestions = arrayOf(
|
||||||
|
"Firefox Suggest",
|
||||||
|
"Amazon.com - Official Site",
|
||||||
|
"Sponsored",
|
||||||
|
),
|
||||||
|
searchTerm = "Amazon",
|
||||||
|
shouldEditKeyword = true,
|
||||||
|
numberOfDeletionSteps = 3,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348374
|
||||||
|
@SmokeTest
|
||||||
|
@Test
|
||||||
|
fun verifyFirefoxSuggestNonSponsoredSearchResultsTest() {
|
||||||
|
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||||
|
navigationToolbar {
|
||||||
|
}.clickUrlbar {
|
||||||
|
typeSearch(searchTerm = "Marvel")
|
||||||
|
verifySearchEngineSuggestionResults(
|
||||||
|
rule = activityTestRule,
|
||||||
|
searchSuggestions = arrayOf(
|
||||||
|
"Firefox Suggest",
|
||||||
|
"Wikipedia - Marvel Cinematic Universe",
|
||||||
|
),
|
||||||
|
searchTerm = "Marvel",
|
||||||
|
)
|
||||||
|
verifySuggestionsAreNotDisplayed(
|
||||||
|
rule = activityTestRule,
|
||||||
|
searchSuggestions = arrayOf(
|
||||||
|
"Sponsored",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348375
|
||||||
|
@Test
|
||||||
|
fun verifyFirefoxSuggestNonSponsoredSearchResultsWithPartialKeywordTest() {
|
||||||
|
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||||
|
navigationToolbar {
|
||||||
|
}.clickUrlbar {
|
||||||
|
typeSearch(searchTerm = "Marv")
|
||||||
|
verifySearchEngineSuggestionResults(
|
||||||
|
rule = activityTestRule,
|
||||||
|
searchSuggestions = arrayOf(
|
||||||
|
"Firefox Suggest",
|
||||||
|
"Wikipedia - Marvel Cinematic Universe",
|
||||||
|
),
|
||||||
|
searchTerm = "Marv",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348376
|
||||||
|
@Test
|
||||||
|
fun openFirefoxSuggestNonSponsoredSearchResultsTest() {
|
||||||
|
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
|
||||||
|
navigationToolbar {
|
||||||
|
}.clickUrlbar {
|
||||||
|
typeSearch(searchTerm = "Marvel")
|
||||||
|
verifySearchEngineSuggestionResults(
|
||||||
|
rule = activityTestRule,
|
||||||
|
searchSuggestions = arrayOf(
|
||||||
|
"Firefox Suggest",
|
||||||
|
"Wikipedia - Marvel Cinematic Universe",
|
||||||
|
),
|
||||||
|
searchTerm = "Marvel",
|
||||||
|
)
|
||||||
|
}.clickSearchSuggestion("Wikipedia - Marvel Cinematic Universe") {
|
||||||
|
waitForPageToLoad()
|
||||||
|
verifyUrl(
|
||||||
|
"wikipedia.org/wiki/Marvel_Cinematic_Universe",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.ui
|
||||||
|
|
||||||
|
import okhttp3.mockwebserver.MockWebServer
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
|
||||||
|
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
|
||||||
|
import org.mozilla.fenix.helpers.TestAssetHelper.TestAsset
|
||||||
|
import org.mozilla.fenix.helpers.TestAssetHelper.getGPCTestAsset
|
||||||
|
import org.mozilla.fenix.ui.robots.homeScreen
|
||||||
|
import org.mozilla.fenix.ui.robots.navigationToolbar
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for Global Privacy Control setting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class GlobalPrivacyControlTest {
|
||||||
|
private lateinit var mockWebServer: MockWebServer
|
||||||
|
private lateinit var gpcPage: TestAsset
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val activityTestRule = HomeActivityIntentTestRule(
|
||||||
|
isJumpBackInCFREnabled = false,
|
||||||
|
isTCPCFREnabled = false,
|
||||||
|
isWallpaperOnboardingEnabled = false,
|
||||||
|
skipOnboarding = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
mockWebServer = MockWebServer().apply {
|
||||||
|
dispatcher = AndroidAssetDispatcher()
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
gpcPage = getGPCTestAsset(mockWebServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
mockWebServer.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2429327
|
||||||
|
@Test
|
||||||
|
fun testGPCinNormalBrowsing() {
|
||||||
|
navigationToolbar {
|
||||||
|
}.enterURLAndEnterToBrowser(gpcPage.url) {
|
||||||
|
verifyPageContent("GPC not enabled.")
|
||||||
|
}.openThreeDotMenu {
|
||||||
|
}.openSettings {
|
||||||
|
}.openEnhancedTrackingProtectionSubMenu {
|
||||||
|
scrollToGCPSettings()
|
||||||
|
verifyGPCTextWithSwitchWidget()
|
||||||
|
verifyGPCSwitchEnabled(false)
|
||||||
|
switchGPCToggle()
|
||||||
|
}.goBack {
|
||||||
|
}.goBackToBrowser {
|
||||||
|
verifyPageContent("GPC is enabled.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2429364
|
||||||
|
@Test
|
||||||
|
fun testGPCinPrivateBrowsing() {
|
||||||
|
homeScreen { }.togglePrivateBrowsingMode()
|
||||||
|
navigationToolbar {
|
||||||
|
}.enterURLAndEnterToBrowser(gpcPage.url) {
|
||||||
|
verifyPageContent("GPC is enabled.")
|
||||||
|
}.openThreeDotMenu {
|
||||||
|
}.openSettings {
|
||||||
|
}.openEnhancedTrackingProtectionSubMenu {
|
||||||
|
scrollToGCPSettings()
|
||||||
|
verifyGPCTextWithSwitchWidget()
|
||||||
|
verifyGPCSwitchEnabled(false)
|
||||||
|
switchGPCToggle()
|
||||||
|
}.goBack {
|
||||||
|
}.goBackToBrowser {
|
||||||
|
verifyPageContent("GPC is enabled.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ import androidx.test.uiautomator.UiDevice
|
||||||
import okhttp3.mockwebserver.MockWebServer
|
import 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() {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -81,6 +81,7 @@ class SettingsAddonsTest {
|
||||||
) {
|
) {
|
||||||
clickInstallAddon(addonName)
|
clickInstallAddon(addonName)
|
||||||
}
|
}
|
||||||
|
verifyAddonDownloadOverlay()
|
||||||
verifyAddonPermissionPrompt(addonName)
|
verifyAddonPermissionPrompt(addonName)
|
||||||
cancelInstallAddon()
|
cancelInstallAddon()
|
||||||
clickInstallAddon(addonName)
|
clickInstallAddon(addonName)
|
||||||
|
|
|
@ -219,6 +219,10 @@ class SettingsDeleteBrowsingDataTest {
|
||||||
selectOnlyCookiesCheckBox()
|
selectOnlyCookiesCheckBox()
|
||||||
clickDeleteBrowsingDataButton()
|
clickDeleteBrowsingDataButton()
|
||||||
verifyDeleteBrowsingDataDialog()
|
verifyDeleteBrowsingDataDialog()
|
||||||
|
clickDialogCancelButton()
|
||||||
|
verifyCookiesCheckBox(status = true)
|
||||||
|
clickDeleteBrowsingDataButton()
|
||||||
|
verifyDeleteBrowsingDataDialog()
|
||||||
confirmDeletionAndAssertSnackbar()
|
confirmDeletionAndAssertSnackbar()
|
||||||
exitMenu()
|
exitMenu()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
|
@ -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 = "Don’t allow")
|
assertItemTextEquals(denyPagePermissionButton, expectedText = "Don’t 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 = "Don’t allow")
|
assertItemTextEquals(denyPagePermissionButton, expectedText = "Don’t allow")
|
||||||
assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow")
|
assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow")
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,16 @@
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_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" />
|
||||||
|
|
|
@ -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">Сұранымды "%s" жолымен алмастырыңыз. Мысалы:\nhttps://leosearch.ddns.net/search?q=%s</string>
|
res/values-kk/strings.xml:1956: <string name="search_add_custom_engine_search_string_example" formatted="false">Сұранымды "%s" жолымен алмастырыңыз. Мысалы:\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">Disodli’r 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">Disodli’r 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 "%s". 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 "%s". 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 "%s" 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 "%s" 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 "%s". 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 "%s". 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\'uî\'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\'uî\'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">ጥያቄውን በ "%s" ይተኩ። ምሳሌ፡- \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">ጥያቄውን በ "%s" ይተኩ። ምሳሌ፡- \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">"%s" দিয়ে কোয়েরি প্রতিস্থাপন করুন। উদাহরণ: \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">"%s" দিয়ে কোয়েরি প্রতিস্থাপন করুন। উদাহরণ: \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>
|
||||||
|
|
|
@ -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">Сұранымды "%s" жолымен алмастырыңыз. Мысалы:\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">Disodli’r 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 "%s". 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 "%s" 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 "%s". 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\'uî\'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">ጥያቄውን በ "%s" ይተኩ። ምሳሌ፡- \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">"%s" দিয়ে কোয়েরি প্রতিস্থাপন করুন। উদাহরণ: \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 |
|
@ -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,8 +39,18 @@ 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(
|
|
||||||
|
return services.urlRequestInterceptor.onLoadRequest(
|
||||||
|
engineSession,
|
||||||
|
uri,
|
||||||
|
lastUri,
|
||||||
|
hasUserGesture,
|
||||||
|
isSameDomain,
|
||||||
|
isRedirect,
|
||||||
|
isDirectNavigation,
|
||||||
|
isSubframeRequest,
|
||||||
|
) ?: services.appLinksInterceptor.onLoadRequest(
|
||||||
engineSession,
|
engineSession,
|
||||||
uri,
|
uri,
|
||||||
lastUri,
|
lastUri,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 = desktopRootNode.count()
|
settings().desktopBookmarksSize = desktopFolders.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,8 +662,11 @@ 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)
|
||||||
|
|
||||||
|
if (this !is ExternalAppBrowserActivity) {
|
||||||
stopMediaSession()
|
stopMediaSession()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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())
|
fun fromUri(uri: Uri): File {
|
||||||
file.close()
|
|
||||||
mis.close()
|
|
||||||
return al
|
|
||||||
}
|
|
||||||
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)!!
|
||||||
|
|
|
@ -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,12 +88,14 @@ 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 ->
|
||||||
|
val extURI = requireComponents.intentProcessors.addonInstallIntentProcessor.parseExtension(tmpFile)
|
||||||
requireComponents.intentProcessors.addonInstallIntentProcessor.installExtension(
|
requireComponents.intentProcessors.addonInstallIntentProcessor.installExtension(
|
||||||
ext[0], ext[1],
|
extURI,
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
val ao = Addon.newFromWebExtension(it)
|
val installedState = provideAddonManger().toInstalledState(it)
|
||||||
|
val ao = Addon.newFromWebExtension(it, installedState)
|
||||||
runIfFragmentIsAttached {
|
runIfFragmentIsAttached {
|
||||||
adapter?.updateAddon(ao)
|
adapter?.updateAddon(ao)
|
||||||
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
|
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
|
||||||
|
@ -103,7 +108,6 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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(
|
||||||
|
FragmentInstalledAddOnDetailsBinding.inflate(
|
||||||
inflater,
|
inflater,
|
||||||
container,
|
container,
|
||||||
false,
|
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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -207,7 +207,7 @@ class Components(private val context: Context) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val fxSuggest by lazyMonitored { FxSuggest(context) }
|
val fxSuggest by lazyMonitored { FxSuggest(context, analytics.crashReporter) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -142,11 +142,13 @@ class Core(
|
||||||
R.color.fx_mobile_layer_color_1,
|
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(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.components
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
|
||||||
|
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.ALLOW_ADDITIONAL_HEADERS
|
||||||
|
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE
|
||||||
|
import mozilla.components.concept.engine.request.RequestInterceptor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [RequestInterceptor] implementation for intercepting URL load requests to allow custom
|
||||||
|
* behaviour.
|
||||||
|
*
|
||||||
|
* @param isDeviceRamAboveThreshold Whether or not the device ram is above a threshold.
|
||||||
|
*/
|
||||||
|
class UrlRequestInterceptor(private val isDeviceRamAboveThreshold: Boolean) : RequestInterceptor {
|
||||||
|
|
||||||
|
private val isGoogleSearchRequest by lazy {
|
||||||
|
Regex("^https://www\\.google\\.(?:.+)/search")
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun getAdditionalHeaders(isDeviceRamAboveThreshold: Boolean): Map<String, String> {
|
||||||
|
val value = if (isDeviceRamAboveThreshold) {
|
||||||
|
"1"
|
||||||
|
} else {
|
||||||
|
"0"
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapOf(
|
||||||
|
"X-Search-Subdivision" to value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun shouldInterceptRequest(
|
||||||
|
uri: String,
|
||||||
|
isSubframeRequest: Boolean,
|
||||||
|
): Boolean {
|
||||||
|
return !isSubframeRequest && isGoogleSearchRequest.containsMatchIn(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoadRequest(
|
||||||
|
engineSession: EngineSession,
|
||||||
|
uri: String,
|
||||||
|
lastUri: String?,
|
||||||
|
hasUserGesture: Boolean,
|
||||||
|
isSameDomain: Boolean,
|
||||||
|
isRedirect: Boolean,
|
||||||
|
isDirectNavigation: Boolean,
|
||||||
|
isSubframeRequest: Boolean,
|
||||||
|
): RequestInterceptor.InterceptionResponse? {
|
||||||
|
if (!shouldInterceptRequest(uri = uri, isSubframeRequest = isSubframeRequest)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return RequestInterceptor.InterceptionResponse.Url(
|
||||||
|
url = uri,
|
||||||
|
flags = LoadUrlFlags.select(
|
||||||
|
LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE,
|
||||||
|
ALLOW_ADDITIONAL_HEADERS,
|
||||||
|
),
|
||||||
|
additionalHeaders = getAdditionalHeaders(isDeviceRamAboveThreshold),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
|
||||||
import org.mozilla.fenix.browser.StandardSnackbarError
|
import org.mozilla.fenix.browser.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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,6 +328,34 @@ 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,
|
||||||
-> {
|
-> {
|
||||||
|
val impressionInfo = metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO)
|
||||||
|
val engagementAbandoned = metadata?.get(FxSuggestFacts.MetadataKeys.ENGAGEMENT_ABANDONED) as? Boolean
|
||||||
|
?: false
|
||||||
|
|
||||||
|
// Record an event for this impression in the `events` ping. These events include the `client_id`, and
|
||||||
|
// we record them for engaged and abandoned search sessions.
|
||||||
|
when (impressionInfo) {
|
||||||
|
is FxSuggestInteractionInfo.Amp -> {
|
||||||
|
Awesomebar.sponsoredSuggestionImpressed.record(
|
||||||
|
Awesomebar.SponsoredSuggestionImpressedExtra(
|
||||||
|
provider = "amp",
|
||||||
|
engagementAbandoned = engagementAbandoned,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is FxSuggestInteractionInfo.Wikipedia -> {
|
||||||
|
Awesomebar.nonSponsoredSuggestionImpressed.record(
|
||||||
|
Awesomebar.NonSponsoredSuggestionImpressedExtra(
|
||||||
|
provider = "wikipedia",
|
||||||
|
engagementAbandoned = engagementAbandoned,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
FxSuggest.pingType.set("fxsuggest-impression")
|
||||||
(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)?.let {
|
(metadata?.get(FxSuggestFacts.MetadataKeys.IS_CLICKED) as? Boolean)?.let {
|
||||||
FxSuggest.isClicked.set(it)
|
FxSuggest.isClicked.set(it)
|
||||||
|
@ -314,7 +363,7 @@ internal class ReleaseMetricController(
|
||||||
(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 impressionInfo = metadata?.get(FxSuggestFacts.MetadataKeys.INTERACTION_INFO)) {
|
when (impressionInfo) {
|
||||||
is FxSuggestInteractionInfo.Amp -> {
|
is FxSuggestInteractionInfo.Amp -> {
|
||||||
FxSuggest.blockId.set(impressionInfo.blockId)
|
FxSuggest.blockId.set(impressionInfo.blockId)
|
||||||
FxSuggest.advertiser.set(impressionInfo.advertiser)
|
FxSuggest.advertiser.set(impressionInfo.advertiser)
|
||||||
|
@ -330,6 +379,9 @@ internal class ReleaseMetricController(
|
||||||
Pings.fxSuggest.submit()
|
Pings.fxSuggest.submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
|
||||||
Component.FEATURE_PWA to ProgressiveWebAppFacts.Items.HOMESCREEN_ICON_TAP -> {
|
Component.FEATURE_PWA to ProgressiveWebAppFacts.Items.HOMESCREEN_ICON_TAP -> {
|
||||||
ProgressiveWebApp.homescreenTap.record(NoExtras())
|
ProgressiveWebApp.homescreenTap.record(NoExtras())
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,212 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.components.metrics.fonts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.fonts.Font
|
||||||
|
import android.graphics.fonts.SystemFonts
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.LocaleList
|
||||||
|
import androidx.work.BackoffPolicy
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.mozilla.fenix.Config
|
||||||
|
import org.mozilla.fenix.GleanMetrics.Metrics
|
||||||
|
import org.mozilla.fenix.GleanMetrics.Pings
|
||||||
|
import org.mozilla.fenix.ext.settings
|
||||||
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse all of the fonts on the user's phone, then put them into the
|
||||||
|
* `font_list_json` Metric to be submitted via Telemetry later.
|
||||||
|
*/
|
||||||
|
class FontEnumerationWorker(
|
||||||
|
context: Context,
|
||||||
|
workerParameters: WorkerParameters,
|
||||||
|
) : CoroutineWorker(context, workerParameters) {
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
||||||
|
val s: String
|
||||||
|
try {
|
||||||
|
readAllFonts()
|
||||||
|
s = createJSONString()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return@withContext Result.retry()
|
||||||
|
}
|
||||||
|
|
||||||
|
Metrics.fontListJson.set(s)
|
||||||
|
Pings.fontList.submit()
|
||||||
|
|
||||||
|
// To avoid getting multiple submissions from new installs, set directly
|
||||||
|
// to the desired number of submissions
|
||||||
|
applicationContext.settings().numFontListSent = kDesiredSubmissions
|
||||||
|
|
||||||
|
return@withContext Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val brokenFonts: ArrayList<Pair<String, String>> = ArrayList()
|
||||||
|
private val fonts: MutableSet<FontMetric> = HashSet()
|
||||||
|
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
private fun readAllFonts() {
|
||||||
|
for (path in getSystemFonts()) {
|
||||||
|
try {
|
||||||
|
fonts.add(FontParser.parse(path))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
brokenFonts.add(Pair(path, FontParser.calculateFileHash(path)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (path in getAPIFonts()) {
|
||||||
|
try {
|
||||||
|
fonts.add(FontParser.parse(path))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
brokenFonts.add(Pair(path, FontParser.calculateFileHash(path)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function creates a single JSON String containing
|
||||||
|
* The user's phone information, as well as all the fonts and their information,
|
||||||
|
* And the names of files that encountered a parsing error.
|
||||||
|
*/
|
||||||
|
@Throws(JSONException::class)
|
||||||
|
fun createJSONString(): String {
|
||||||
|
val submission = JSONObject()
|
||||||
|
|
||||||
|
run {
|
||||||
|
submission.put("submission", kDesiredSubmissions)
|
||||||
|
submission.put("brand", Build.BRAND)
|
||||||
|
submission.put("device", Build.DEVICE)
|
||||||
|
submission.put("hardware", Build.HARDWARE)
|
||||||
|
submission.put("manufacturer", Build.MANUFACTURER)
|
||||||
|
submission.put("model", Build.MODEL)
|
||||||
|
submission.put("product", Build.PRODUCT)
|
||||||
|
submission.put("release_version", Build.VERSION.RELEASE)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
submission.put("security_patch", Build.VERSION.SECURITY_PATCH)
|
||||||
|
submission.put("base_os", Build.VERSION.BASE_OS)
|
||||||
|
} else {
|
||||||
|
submission.put("security_patch", "too-low-version")
|
||||||
|
submission.put("base_os", "too-low-version")
|
||||||
|
}
|
||||||
|
val config: Configuration = this.applicationContext.resources.configuration
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
val supportedLocales: LocaleList = LocaleList.getDefault()
|
||||||
|
val sb = StringBuilder()
|
||||||
|
for (i in 0 until supportedLocales.size()) {
|
||||||
|
val locale: Locale = supportedLocales.get(i)
|
||||||
|
sb.append(locale.toString())
|
||||||
|
sb.append(",")
|
||||||
|
}
|
||||||
|
submission.put("current_locale", config.locales[0].toString())
|
||||||
|
submission.put("all_locales", sb.toString())
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
submission.put("current_locale", config.locale.toString())
|
||||||
|
submission.put("all_locales", "too-low-version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fontArr = JSONArray()
|
||||||
|
for (fontDetails in fonts) {
|
||||||
|
fontArr.put(fontDetails.toJson())
|
||||||
|
}
|
||||||
|
|
||||||
|
val errorArr = JSONArray()
|
||||||
|
for (error in brokenFonts) {
|
||||||
|
val errorObj = JSONObject()
|
||||||
|
errorObj.put("path", error.first)
|
||||||
|
errorObj.put("hash", error.second)
|
||||||
|
errorArr.put(errorObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
submission.put("fonts", fontArr)
|
||||||
|
submission.put("errors", errorArr)
|
||||||
|
|
||||||
|
return submission.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val FONT_ENUMERATOR_WORK_NAME = "org.mozilla.fenix.metrics.font.work"
|
||||||
|
private val HOUR_MILLIS: Long = 1.hours.inWholeMilliseconds
|
||||||
|
private const val SIX: Long = 6
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules the Activated User event if needed.
|
||||||
|
*/
|
||||||
|
fun sendActivatedSignalIfNeeded(context: Context) {
|
||||||
|
val instanceWorkManager = WorkManager.getInstance(context)
|
||||||
|
|
||||||
|
if (!Config.channel.isNightlyOrDebug) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.settings().numFontListSent >= kDesiredSubmissions) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val fontEnumeratorWork =
|
||||||
|
OneTimeWorkRequest.Builder(FontEnumerationWorker::class.java)
|
||||||
|
.setInitialDelay(HOUR_MILLIS, TimeUnit.MILLISECONDS)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, SIX, TimeUnit.HOURS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
instanceWorkManager.beginUniqueWork(
|
||||||
|
FONT_ENUMERATOR_WORK_NAME,
|
||||||
|
ExistingWorkPolicy.KEEP,
|
||||||
|
fontEnumeratorWork,
|
||||||
|
).enqueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSystemFonts(): ArrayList<String> {
|
||||||
|
val file = File("/system/fonts")
|
||||||
|
val ff: Array<out File>? = file.listFiles()
|
||||||
|
val systemFonts: ArrayList<String> = ArrayList()
|
||||||
|
if (ff != null) {
|
||||||
|
for (f in ff) {
|
||||||
|
systemFonts.add(f.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return systemFonts
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAPIFonts(): List<String> {
|
||||||
|
val aPIFonts: List<String>
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
aPIFonts = emptyList()
|
||||||
|
} else {
|
||||||
|
aPIFonts = ArrayList()
|
||||||
|
val apiFonts: Set<Font> = SystemFonts.getAvailableFonts()
|
||||||
|
for (f in apiFonts) {
|
||||||
|
f.file?.let {
|
||||||
|
aPIFonts.add(it.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aPIFonts
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of font submissions we would like from a user.
|
||||||
|
* We will increment this number by one (via a code patch) when
|
||||||
|
* we wish to perform another data collection effort on the Nightly
|
||||||
|
* population.
|
||||||
|
*/
|
||||||
|
const val kDesiredSubmissions: Int = 4
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,253 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.components.metrics.fonts
|
||||||
|
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.DataInputStream
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FontMetric represents the information about a Font File
|
||||||
|
*/
|
||||||
|
data class FontMetric(
|
||||||
|
val path: String = "",
|
||||||
|
val hash: String = "",
|
||||||
|
) {
|
||||||
|
var family: String = ""
|
||||||
|
var subFamily: String = ""
|
||||||
|
var uniqueSubFamily: String = ""
|
||||||
|
var fullName: String = ""
|
||||||
|
var fontVersion: String = ""
|
||||||
|
var revision: Int = -1
|
||||||
|
var created: Long = -1L
|
||||||
|
var modified: Long = -1L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a JSONObject of this Font's details
|
||||||
|
*/
|
||||||
|
fun toJson(): JSONObject {
|
||||||
|
val jsonObject = JSONObject()
|
||||||
|
try {
|
||||||
|
// Use abbreviations to make the json smaller
|
||||||
|
jsonObject.put("F", family.replace("\u0000", ""))
|
||||||
|
jsonObject.put("SF", subFamily.replace("\u0000", ""))
|
||||||
|
jsonObject.put("USF", uniqueSubFamily.replace("\u0000", ""))
|
||||||
|
jsonObject.put("FN", fullName.replace("\u0000", ""))
|
||||||
|
jsonObject.put("V", fontVersion.replace("\u0000", ""))
|
||||||
|
jsonObject.put("R", revision)
|
||||||
|
jsonObject.put("C", created)
|
||||||
|
jsonObject.put("M", modified)
|
||||||
|
jsonObject.put("H", hash)
|
||||||
|
jsonObject.put("P", path.replace("\u0000", ""))
|
||||||
|
} catch (_: JSONException) {
|
||||||
|
}
|
||||||
|
return jsonObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a font, given via an InputStream, to extract the Font information
|
||||||
|
* including Family, SubFamily, Revision, etc
|
||||||
|
*/
|
||||||
|
object FontParser {
|
||||||
|
/**
|
||||||
|
* Parse a font file and return a FontMetric object describing it.
|
||||||
|
* These functions are very similar, because this one is used in
|
||||||
|
* real devices, the other in unit tests. Outside tests, the
|
||||||
|
* FileInputStream does not support the reset() method
|
||||||
|
*/
|
||||||
|
fun parse(path: String): FontMetric {
|
||||||
|
val hash = calculateFileHash(FileInputStream(path))
|
||||||
|
val fontDetails = FontMetric(path, hash)
|
||||||
|
|
||||||
|
readFontFile(FileInputStream(path), fontDetails)
|
||||||
|
|
||||||
|
return fontDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a font file and return a FontMetric object describing it
|
||||||
|
*/
|
||||||
|
fun parse(path: String, inputStream: InputStream): FontMetric {
|
||||||
|
val hash = calculateFileHash(inputStream)
|
||||||
|
val fontDetails = FontMetric(path, hash)
|
||||||
|
|
||||||
|
inputStream.reset()
|
||||||
|
readFontFile(inputStream, fontDetails)
|
||||||
|
|
||||||
|
return fontDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
private fun readFontFile(inputStream: InputStream, fontDetails: FontMetric) {
|
||||||
|
val file = DataInputStream(inputStream)
|
||||||
|
val numFonts: Int
|
||||||
|
val magicNumber = file.readInt()
|
||||||
|
var bytesReadSoFar = 4
|
||||||
|
|
||||||
|
if (magicNumber == 0x74746366) {
|
||||||
|
// The Font File has a TTC Header
|
||||||
|
val majorVersion = file.readUnsignedShort()
|
||||||
|
file.skipBytes(2) // Minor Version
|
||||||
|
numFonts = file.readInt()
|
||||||
|
bytesReadSoFar += 8
|
||||||
|
|
||||||
|
file.skipBytes(4 * numFonts) // OffsetTable
|
||||||
|
bytesReadSoFar += 4 * numFonts
|
||||||
|
if (majorVersion == 2) {
|
||||||
|
file.skipBytes(12)
|
||||||
|
bytesReadSoFar += 12
|
||||||
|
}
|
||||||
|
|
||||||
|
file.skipBytes(4) // Magic Number for the Font
|
||||||
|
bytesReadSoFar += 4
|
||||||
|
}
|
||||||
|
val numTables: Int = file.readUnsignedShort()
|
||||||
|
bytesReadSoFar += 2
|
||||||
|
file.skipBytes(6) // Rest of header
|
||||||
|
bytesReadSoFar += 6
|
||||||
|
|
||||||
|
// Find the head table
|
||||||
|
var headOffset = 0
|
||||||
|
var nameOffset = 0
|
||||||
|
var nameLength = 0
|
||||||
|
for (i in 0 until numTables) {
|
||||||
|
val tableName =
|
||||||
|
CharArray(4) {
|
||||||
|
file.readUnsignedByte().toChar()
|
||||||
|
}
|
||||||
|
file.skipBytes(4) // checksum
|
||||||
|
val offset = file.readInt() // technically it's unsigned but we should be okay
|
||||||
|
val length = file.readInt() // technically it's unsigned but we should be okay
|
||||||
|
|
||||||
|
bytesReadSoFar += 16
|
||||||
|
|
||||||
|
if (String(tableName) == "head") {
|
||||||
|
headOffset = offset
|
||||||
|
} else if (String(tableName) == "name") {
|
||||||
|
nameOffset = offset
|
||||||
|
nameLength = length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headOffset == 0 || nameOffset == 0) {
|
||||||
|
throw IOException("Could not find head or name table")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headOffset < nameOffset) {
|
||||||
|
file.skipBytes(headOffset - bytesReadSoFar)
|
||||||
|
bytesReadSoFar = headOffset
|
||||||
|
bytesReadSoFar += readHeadTable(file, fontDetails)
|
||||||
|
file.skipBytes(nameOffset - bytesReadSoFar)
|
||||||
|
readNameTable(file, nameLength, fontDetails)
|
||||||
|
} else {
|
||||||
|
file.skipBytes(nameOffset - bytesReadSoFar)
|
||||||
|
bytesReadSoFar = nameOffset
|
||||||
|
bytesReadSoFar += readNameTable(file, nameLength, fontDetails)
|
||||||
|
file.skipBytes(headOffset - bytesReadSoFar)
|
||||||
|
readHeadTable(file, fontDetails)
|
||||||
|
}
|
||||||
|
file.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
private fun readHeadTable(file: DataInputStream, fontDetails: FontMetric): Int {
|
||||||
|
// Find the details in the head table
|
||||||
|
file.skipBytes(4) // Fixed version
|
||||||
|
fontDetails.revision = file.readInt()
|
||||||
|
file.skipBytes(12) // checksum, magic, flags, units
|
||||||
|
fontDetails.created = file.readLong()
|
||||||
|
fontDetails.modified = file.readLong()
|
||||||
|
return 36
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
private fun readNameTable(
|
||||||
|
file: DataInputStream,
|
||||||
|
tableLength: Int,
|
||||||
|
fontDetails: FontMetric,
|
||||||
|
): Int {
|
||||||
|
file.skipBytes(2) // format
|
||||||
|
val numNames = file.readUnsignedShort()
|
||||||
|
val stringOffset = file.readUnsignedShort()
|
||||||
|
var bytesReadSoFar = 6
|
||||||
|
val nameTable = arrayListOf<Triple<Int, Int, Int>>()
|
||||||
|
|
||||||
|
for (i in 0 until numNames) {
|
||||||
|
file.skipBytes(6) // platform id, encoding id, langid
|
||||||
|
val nameID = file.readUnsignedShort()
|
||||||
|
val length = file.readUnsignedShort()
|
||||||
|
val offset = file.readUnsignedShort()
|
||||||
|
nameTable.add(Triple(nameID, length, offset))
|
||||||
|
bytesReadSoFar += 12
|
||||||
|
}
|
||||||
|
|
||||||
|
val stringTableSize = min(tableLength - bytesReadSoFar, tableLength - stringOffset)
|
||||||
|
val stringTable = ByteArray(stringTableSize)
|
||||||
|
|
||||||
|
if (stringTable.size != file.read(stringTable)) {
|
||||||
|
throw IOException("Did not read entire string table")
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesReadSoFar += stringTable.size
|
||||||
|
|
||||||
|
// Now we're at the beginning of the string table
|
||||||
|
for (i in nameTable) {
|
||||||
|
when (i.first) {
|
||||||
|
1 -> fontDetails.family = getString(stringTable, i.third, i.second)
|
||||||
|
2 -> fontDetails.subFamily = getString(stringTable, i.third, i.second)
|
||||||
|
3 -> fontDetails.uniqueSubFamily = getString(stringTable, i.third, i.second)
|
||||||
|
4 -> fontDetails.fullName = getString(stringTable, i.third, i.second)
|
||||||
|
5 -> fontDetails.fontVersion = getString(stringTable, i.third, i.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytesReadSoFar
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getString(
|
||||||
|
stringTable: ByteArray,
|
||||||
|
offset: Int,
|
||||||
|
length: Int,
|
||||||
|
): String {
|
||||||
|
return String(stringTable.copyOfRange(offset, offset + length))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the SHA-256 hash of the file passed
|
||||||
|
*/
|
||||||
|
fun calculateFileHash(path: String): String {
|
||||||
|
return calculateFileHash(FileInputStream(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the SHA-256 hash of the InputStream passed
|
||||||
|
*/
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
private fun calculateFileHash(inputStream: InputStream): String {
|
||||||
|
try {
|
||||||
|
val md = MessageDigest.getInstance("SHA-256")
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
var bytesRead: Int
|
||||||
|
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||||
|
md.update(buffer, 0, bytesRead)
|
||||||
|
}
|
||||||
|
val digest = md.digest()
|
||||||
|
// Convert the byte array to a hexadecimal string
|
||||||
|
val hashBuilder = StringBuilder()
|
||||||
|
for (b in digest) {
|
||||||
|
hashBuilder.append(String.format("%02X", b))
|
||||||
|
}
|
||||||
|
return hashBuilder.toString()
|
||||||
|
} catch (_: NoSuchAlgorithmException) {
|
||||||
|
return "sha-256-not-found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ import org.mozilla.fenix.browser.readermode.ReaderModeController
|
||||||
import org.mozilla.fenix.components.toolbar.interactor.BrowserToolbarInteractor
|
import org.mozilla.fenix.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 {
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.components.toolbar
|
||||||
|
|
||||||
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstraction for the Toolbar Redesign feature.
|
||||||
|
*/
|
||||||
|
interface RedesignToolbarFeature {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the toolbar redesign feature is enabled.
|
||||||
|
*/
|
||||||
|
val isEnabled: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The complete portions of the redesigned Toolbar ready for Nightly.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class CompleteRedesignToolbarFeature(
|
||||||
|
private val settings: Settings,
|
||||||
|
) : RedesignToolbarFeature {
|
||||||
|
|
||||||
|
override val isEnabled: Boolean
|
||||||
|
get() = settings.enableRedesignToolbar
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The incomplete portions of the redesigned Toolbar still in progress.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class IncompleteRedesignToolbarFeature(
|
||||||
|
private val settings: Settings,
|
||||||
|
) : RedesignToolbarFeature {
|
||||||
|
|
||||||
|
override val isEnabled: Boolean
|
||||||
|
get() = settings.enableIncompleteToolbarRedesign
|
||||||
|
}
|
|
@ -35,6 +35,10 @@ import org.mozilla.fenix.theme.FirefoxTheme
|
||||||
* bounds defined by the width and height.
|
* 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.debugsettings.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [DataStore] for accessing debugging settings.
|
||||||
|
*/
|
||||||
|
private val Context.debugSettings: DataStore<Preferences> by preferencesDataStore(name = "debug_settings")
|
||||||
|
|
||||||
|
private val debugDrawerEnabledKey = booleanPreferencesKey("debug_drawer_enabled")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache for accessing any settings related to debugging.
|
||||||
|
*/
|
||||||
|
interface DebugSettingsRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Flow] for checking whether the Debug Drawer is enabled.
|
||||||
|
*/
|
||||||
|
val debugDrawerEnabled: Flow<Boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates whether the debug drawer is enabled.
|
||||||
|
*
|
||||||
|
* @param enabled Whether the debug drawer is enabled.
|
||||||
|
*/
|
||||||
|
fun setDebugDrawerEnabled(enabled: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default implementation of [DebugSettingsRepository].
|
||||||
|
*
|
||||||
|
* @param context Android context used to obtain the underlying [DataStore].
|
||||||
|
* @param dataStore [DataStore] for accessing debugging settings.
|
||||||
|
* @param writeScope [CoroutineScope] used for writing settings changes to disk.
|
||||||
|
*/
|
||||||
|
class DefaultDebugSettingsRepository(
|
||||||
|
context: Context,
|
||||||
|
private val dataStore: DataStore<Preferences> = context.debugSettings,
|
||||||
|
private val writeScope: CoroutineScope,
|
||||||
|
) : DebugSettingsRepository {
|
||||||
|
|
||||||
|
override val debugDrawerEnabled: Flow<Boolean> =
|
||||||
|
dataStore.data.map { preferences ->
|
||||||
|
preferences[debugDrawerEnabledKey] ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDebugDrawerEnabled(enabled: Boolean) {
|
||||||
|
writeScope.launch {
|
||||||
|
dataStore.edit { preferences ->
|
||||||
|
preferences[debugDrawerEnabledKey] = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.debugsettings.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Snackbar
|
||||||
|
import androidx.compose.material.SnackbarHost
|
||||||
|
import androidx.compose.material.SnackbarHostState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.compose.annotation.LightDarkPreview
|
||||||
|
import org.mozilla.fenix.compose.button.FloatingActionButton
|
||||||
|
import org.mozilla.fenix.theme.FirefoxTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overlay for presenting Fenix-wide debugging content.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DebugOverlay() {
|
||||||
|
val snackbarState = remember { SnackbarHostState() }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
FloatingActionButton(
|
||||||
|
icon = painterResource(R.drawable.ic_debug_transparent_fire_24),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
snackbarState.showSnackbar("Show debug drawer")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// This must be the last element in the Box
|
||||||
|
SnackbarHost(
|
||||||
|
hostState = snackbarState,
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter),
|
||||||
|
) { snackbarData ->
|
||||||
|
Snackbar(
|
||||||
|
snackbarData = snackbarData,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@LightDarkPreview
|
||||||
|
private fun DebugOverlayPreview() {
|
||||||
|
FirefoxTheme {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(color = FirefoxTheme.colors.layer1),
|
||||||
|
) {
|
||||||
|
DebugOverlay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
|
@ -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.
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
Binary file not shown.
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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? {
|
||||||
|
|
|
@ -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) -> {
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue