Merge pull request #245 from fork-maintainers/upstream-sync2

Upstream Sync 2: The Syncening
pull/293/head
interfect 4 years ago committed by GitHub
commit 6d5f903652
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -49,6 +49,7 @@
# Possible startup regressions # Possible startup regressions
*Application.kt @mozilla-mobile/Performance *Application.kt @mozilla-mobile/Performance
*StrictMode*kt @mozilla-mobile/Performance *StrictMode*kt @mozilla-mobile/Performance
*ConstraintLayoutPerfDetector.kt @mozilla-mobile/Performance
# We want to be aware of new features behind flags as well as features # We want to be aware of new features behind flags as well as features
# about to be enabled. # about to be enabled.

@ -6,6 +6,9 @@ labels: "\U0001F41E bug"
assignees: '' assignees: ''
--- ---
[comment]: # (Please do your best to search for duplicate issues before filing a new issue so we can keep our issue board clean)
[comment]: # (Every issue should have the exact bug and steps to reproduce described in it. Please do not file feedback list tickets as it is difficult to parse them and address their individual points)
[comment]: # (Read https://github.com/mozilla-mobile/fenix#i-want-to-file-an-issue for more information)
## Steps to reproduce ## Steps to reproduce
@ -17,3 +20,4 @@ assignees: ''
* Android device: ? * Android device: ?
* Fenix version: ? * Fenix version: ?

@ -6,6 +6,10 @@ labels: "🌟 feature request"
assignees: '' assignees: ''
--- ---
[comment]: # (Please do your best to search for duplicate issues before filing a new issue so we can keep our issue board clean)
[comment]: # (Every issue should have exactly one feature request described in it. Please do not file feedback list tickets as it is difficult to parse them and address their individual points)
[comment]: # (Feature Requests are better when theyre open-ended instead of demanding a specific solution e.g: “I want an easier way to do X” instead of “add Y”)
[comment]: # (Read https://github.com/mozilla-mobile/fenix#i-want-to-file-an-issue for more information)
### What is the user problem or growth opportunity you want to see solved? ### What is the user problem or growth opportunity you want to see solved?

@ -16,6 +16,8 @@ jobs:
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 11
- name: Install Android SDK with pieces Gradle skips
run: ./automation/iceraven/install-sdk.sh
- name: Create version name - name: Create version name
run: echo "VERSION_NAME=$(git describe --tags HEAD)" >> $GITHUB_ENV run: echo "VERSION_NAME=$(git describe --tags HEAD)" >> $GITHUB_ENV
- name: Build forkRelease variant of app - name: Build forkRelease variant of app

@ -16,6 +16,8 @@ jobs:
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 11
- name: Install Android SDK with pieces Gradle skips
run: ./automation/iceraven/install-sdk.sh
- name: Create version name - name: Create version name
run: echo "VERSION_NAME=$(git describe --tags HEAD)" >> $GITHUB_ENV run: echo "VERSION_NAME=$(git describe --tags HEAD)" >> $GITHUB_ENV
- name: Build forkRelease variant of app - name: Build forkRelease variant of app

@ -15,6 +15,8 @@ jobs:
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 11
- name: Install Android SDK with pieces Gradle skips
run: ./automation/iceraven/install-sdk.sh
- name: Build forkRelease variant of app - name: Build forkRelease variant of app
uses: eskatos/gradle-command-action@v1 uses: eskatos/gradle-command-action@v1
with: with:

@ -1,11 +1,14 @@
language: android language: android
dist: trusty dist: trusty
script: script:
# Prepare SDK
- sudo mkdir -p /usr/local/android-sdk/licenses/ - sudo mkdir -p /usr/local/android-sdk/licenses/
- sudo touch /usr/local/android-sdk/licenses/android-sdk-license - sudo touch /usr/local/android-sdk/licenses/android-sdk-license
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" | sudo tee -a /usr/local/android-sdk/licenses/android-sdk-license - echo "8933bad161af4178b1185d1a37fbf41ea5269c55" | sudo tee -a /usr/local/android-sdk/licenses/android-sdk-license
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" | sudo tee -a /usr/local/android-sdk/licenses/android-sdk-license - echo "d56f5187479451eabf01fb78af6dfcb131a6481e" | sudo tee -a /usr/local/android-sdk/licenses/android-sdk-license
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" | sudo tee -a /usr/local/android-sdk/licenses/android-sdk-license - echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" | sudo tee -a /usr/local/android-sdk/licenses/android-sdk-license
# The build needs this but Gradle refuses to fetch it.
- sdkmanager "ndk;21.0.6113669"
# Run tests # Run tests
- ./gradlew -q testDebug 2>&1 - ./gradlew -q testDebug 2>&1
# Make sure a release build builds # Make sure a release build builds

@ -31,7 +31,7 @@ android {
resValue "bool", "IS_DEBUG", "false" resValue "bool", "IS_DEBUG", "false"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "false" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "false"
buildConfigField "String", "AMO_ACCOUNT", "\"mozilla\"" buildConfigField "String", "AMO_ACCOUNT", "\"mozilla\""
buildConfigField "String", "AMO_COLLECTION", "\"83a9cccfe6e24a34bd7b155ff9ee32\"" buildConfigField "String", "AMO_COLLECTION", "\"7dfae8669acc4312a65e8ba5553036\""
def deepLinkSchemeValue = "fenix-dev" def deepLinkSchemeValue = "fenix-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [ manifestPlaceholders = [
@ -59,14 +59,12 @@ android {
shrinkResources false shrinkResources false
minifyEnabled false minifyEnabled false
applicationIdSuffix ".fenix.debug" applicationIdSuffix ".fenix.debug"
buildConfigField "String", "AMO_COLLECTION", "\"7dfae8669acc4312a65e8ba5553036\""
resValue "bool", "IS_DEBUG", "true" resValue "bool", "IS_DEBUG", "true"
pseudoLocalesEnabled true pseudoLocalesEnabled true
} }
nightly releaseTemplate >> { nightly releaseTemplate >> {
applicationIdSuffix ".fenix" applicationIdSuffix ".fenix"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
buildConfigField "String", "AMO_COLLECTION", "\"7dfae8669acc4312a65e8ba5553036\""
def deepLinkSchemeValue = "fenix-nightly" def deepLinkSchemeValue = "fenix-nightly"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = ["deepLinkScheme": deepLinkSchemeValue] manifestPlaceholders = ["deepLinkScheme": deepLinkSchemeValue]
@ -199,6 +197,7 @@ android {
lintOptions { lintOptions {
lintConfig file("lint.xml") lintConfig file("lint.xml")
baseline file("lint-baseline.xml")
} }
packagingOptions { packagingOptions {
@ -355,7 +354,31 @@ ext.gleanGenerateMarkdownDocs = true
ext.gleanDocsDirectory = "$rootDir/docs" ext.gleanDocsDirectory = "$rootDir/docs"
apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
configurations {
// There's an interaction between Gradle's resolution of dependencies with different types
// (@jar, @aar) for `implementation` and `testImplementation` and with Android Studio's built-in
// JUnit test runner. The runtime classpath in the built-in JUnit test runner gets the
// dependency from the `implementation`, which is type @aar, and therefore the JNA dependency
// doesn't provide the JNI dispatch libraries in the correct Java resource directories. I think
// what's happening is that @aar type in `implementation` resolves to the @jar type in
// `testImplementation`, and that it wins the dependency resolution battle.
//
// A workaround is to add a new configuration which depends on the @jar type and to reference
// the underlying JAR file directly in `testImplementation`. This JAR file doesn't resolve to
// the @aar type in `implementation`. This works when invoked via `gradle`, but also sets the
// correct runtime classpath when invoked with Android Studio's built-in JUnit test runner.
// Success!
jnaForTest
// Robolectric, through `com.google.android.apps.common.testing.accessibility.framework`
// depends on an old version of protobuf that conflict with the Application Services one.
// See: https://github.com/mozilla/application-services/issues/2952
all*.exclude group: 'com.google.protobuf', module: 'protobuf-java'
}
dependencies { dependencies {
jnaForTest Deps.jna
testImplementation files(configurations.jnaForTest.copyRecursive().files)
debugImplementation Deps.mozilla_browser_engine_gecko_nightly debugImplementation Deps.mozilla_browser_engine_gecko_nightly
forkDebugImplementation Deps.mozilla_browser_engine_gecko_nightly forkDebugImplementation Deps.mozilla_browser_engine_gecko_nightly
@ -475,9 +498,6 @@ dependencies {
implementation Deps.androidx_work_ktx implementation Deps.androidx_work_ktx
implementation Deps.google_material implementation Deps.google_material
implementation Deps.lottie
androidTestImplementation Deps.uiautomator androidTestImplementation Deps.uiautomator
// Removed pending AndroidX fixes // Removed pending AndroidX fixes
androidTestImplementation "tools.fastlane:screengrab:2.0.0" androidTestImplementation "tools.fastlane:screengrab:2.0.0"
@ -577,13 +597,13 @@ if (project.hasProperty("coverage")) {
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
tasks.register('printVariants') { tasks.register('printVariants') {
doLast { doLast {
def variants = android.applicationVariants.collect {[ def variants = android.applicationVariants.collect { variant -> [
apks: it.variantData.outputScope.apkDatas.collect {[ apks: variant.outputs.collect { output -> [
abi: it.filters.find { it.filterType == 'ABI' }.identifier, abi: output.getFilter(com.android.build.VariantOutput.FilterType.ABI),
fileName: it.outputFileName, fileName: output.outputFile.name
]}, ]},
build_type: it.buildType.name, build_type: variant.buildType.name,
name: it.name, name: variant.name,
]} ]}
// AndroidTest is a special case not included above // AndroidTest is a special case not included above
variants.add([ variants.add([
@ -605,8 +625,8 @@ task buildTranslationArray {
foundLocales.append("new String[]{") foundLocales.append("new String[]{")
fileTree("src/main/res").visit { FileVisitDetails details -> fileTree("src/main/res").visit { FileVisitDetails details ->
if(details.file.path.endsWith("/strings.xml")){ if(details.file.path.endsWith("${File.separator}strings.xml")){
def languageCode = details.file.parent.tokenize('/').last().replaceAll('values-','').replaceAll('-r','-') def languageCode = details.file.parent.tokenize(File.separator).last().replaceAll('values-','').replaceAll('-r','-')
languageCode = (languageCode == "values") ? "en-US" : languageCode languageCode = (languageCode == "values") ? "en-US" : languageCode
foundLocales.append("\"").append(languageCode).append("\"").append(",") foundLocales.append("\"").append(languageCode).append("\"").append(",")
} }

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<issues format="5" by="lint 3.6.0" client="gradle" variant="debug" version="3.6.0">
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/component_collection_creation.xml"
line="134"
column="6"/>
</issue>
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/component_collection_creation_name_collection.xml"
line="104"
column="6"/>
</issue>
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/component_collection_creation_select_collection.xml"
line="108"
column="6"/>
</issue>
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/component_tabstray.xml"
line="28"
column="6"/>
</issue>
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/component_tabstray.xml"
line="52"
column="6"/>
</issue>
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/component_tracking_protection_panel.xml"
line="15"
column="6"/>
</issue>
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/component_tracking_protection_panel.xml"
line="178"
column="6"/>
</issue>
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/fragment_add_on_details.xml"
line="150"
column="10"/>
</issue>
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/fragment_search.xml"
line="43"
column="10"/>
</issue>
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/fragment_search.xml"
line="82"
column="14"/>
</issue>
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/fragment_search_dialog.xml"
line="77"
column="6"/>
</issue>
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/fragment_share.xml"
line="30"
column="6"/>
</issue>
</issues>

@ -7,16 +7,58 @@
<!-- The Sentry SDK is compiled against parts of the Java SDK that are not available in the Android SDK. <!-- The Sentry SDK is compiled against parts of the Java SDK that are not available in the Android SDK.
Let's just ignore issues in the Sentry code since that is a third-party dependency anyways. --> Let's just ignore issues in the Sentry code since that is a third-party dependency anyways. -->
<ignore path="**/sentry*.jar" /> <ignore path="**/sentry*.jar" />
<!-- Temporary until https://github.com/Kotlin/kotlinx.coroutines/issues/2004 is resolved. -->
<ignore path="**/kotlinx-coroutines-core-*.jar"/>
</issue> </issue>
<!-- Lints that don't apply to our translation process --> <!-- Lints that don't apply to our translation process -->
<issue id="MissingTranslation" severity="ignore" /> <issue id="MissingTranslation" severity="ignore" />
<issue id="PluralsCandidate" severity="ignore" /> <issue id="PluralsCandidate" severity="ignore" />
<issue id="StringFormatCount" severity="ignore" /> <issue id="StringFormatCount" severity="ignore" />
<issue id="TypographyEllipsis" severity="ignore" /> <issue id="TypographyEllipsis" severity="ignore" />
<issue id="ExtraTranslation" severity="warning" /> <issue id="ExtraTranslation" severity="warning" />
<!-- Lints that are disabled by default --> <!-- Lints that are disabled by default -->
<issue id="ConvertToWebp" severity="warning" /> <issue id="ConvertToWebp" severity="warning" />
<!-- Performance: we haven't validated that addressing these checks have a significant impact
on performance but they're very quick to fix so we escalate them to error. -->
<!-- Performance: big wins from a theoretical perspective so we escalate to error. -->
<issue id="DrawAllocation" severity="error" />
<issue id="Wakelock" severity="error" />
<issue id="WakelockTimeout" severity="error" />
<issue id="Recycle" severity="error" />
<issue id="StaticFieldLeak" severity="error" />
<issue id="ViewTag" severity="error" />
<issue id="ViewHolder" severity="error" />
<issue id="HandlerLeak" severity="error" />
<issue id="NestedWeights" severity="error" />
<!-- Performance: quick-to-fix violations so we escalate to error.
We haven't validated that they have a significant impact though. -->
<issue id="ObsoleteLayoutParam" severity="error" />
<issue id="ObsoleteSdkInt" severity="error" />
<issue id="AnimatorKeep" severity="error" />
<issue id="DuplicateDivider" severity="error" />
<issue id="MergeRootFrame" severity="error" />
<issue id="UseOfBundledGooglePlayServices" severity="error" />
<issue id="UseValueOf" severity="error" />
<issue id="InefficientWeight" severity="error" />
<issue id="DisableBaselineAlignment" severity="error" />
<issue id="UselessLeaf" severity="error" />
<issue id="UselessParent" severity="error" />
<issue id="UnusedNamespace" severity="error" />
<!-- Performance: checks we'd like to eventually set to error. -->
<issue id="UseCompoundDrawables" severity="warning" />
<issue id="Overdraw" severity="warning" />
<issue id="UnusedResources" severity="warning" />
<!-- Performance: checks that we're unsure of the value of that we might want to investigate. -->
<issue id="UnpackedNativeCode" severity="informational" />
<issue id="LogConditional" severity="informational" />
<issue id="VectorPath" severity="informational" />
<issue id="UseSparseArrays" severity="informational" /> <!-- hurts developer convenience of kotlin Map... -->
<issue id="TooDeepLayout" severity="warning" /> <!-- depth can be customized -->
<issue id="TooManyViews" severity="warning" /> <!-- view count can be customized -->
</lint> </lint>

@ -279,7 +279,7 @@ events:
whats_new_tapped: whats_new_tapped:
type: event type: event
description: | description: |
A user opened the "what's new" page button A user opened the "what's new" page button
bugs: bugs:
- https://github.com/mozilla-mobile/fenix/issues/5021 - https://github.com/mozilla-mobile/fenix/issues/5021
data_reviews: data_reviews:
@ -947,6 +947,40 @@ metrics:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
close_tab_setting:
type: string
lifetime: application
description: |
A string that indicates the setting for tab closing:
MANUAL, ONE_DAY, ONE_WEEK, ONE_MONTH
send_in_pings:
- metrics
bugs:
- https://github.com/mozilla-mobile/fenix/issues/15347#issue-707408975
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/15811#issuecomment-706402952
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
tab_view_setting:
type: string
lifetime: application
description: |
A string that indicates the setting for tab view:
GRID, LIST
send_in_pings:
- metrics
bugs:
- https://github.com/mozilla-mobile/fenix/issues/15347#issue-707408975
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/15811#issuecomment-706402952
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
search_widget_installed: search_widget_installed:
type: boolean type: boolean
lifetime: application lifetime: application
@ -4207,3 +4241,18 @@ master_password:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-03-01" expires: "2021-03-01"
tabs:
setting_opened:
type: event
description: |
The tab settings were opened.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/15347#issue-707408975
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/15811#issuecomment-706402952
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"

@ -45,6 +45,14 @@
boolean ANDROID_DETECTED return true; boolean ANDROID_DETECTED return true;
} }
####################################################################################################
# Remove debug logs from release builds
####################################################################################################
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int d(...);
}
#################################################################################################### ####################################################################################################
# Mozilla Application Services # Mozilla Application Services
#################################################################################################### ####################################################################################################

@ -15,7 +15,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
import mozilla.components.concept.fetch.Client
import mozilla.components.service.glean.Glean import mozilla.components.service.glean.Glean
import mozilla.components.service.glean.config.Configuration import mozilla.components.service.glean.config.Configuration
import mozilla.components.service.glean.net.ConceptFetchHttpUploader import mozilla.components.service.glean.net.ConceptFetchHttpUploader
@ -78,7 +77,7 @@ class BaselinePingTest {
@JvmStatic @JvmStatic
fun setupOnce() { fun setupOnce() {
val httpClient = ConceptFetchHttpUploader(lazy { val httpClient = ConceptFetchHttpUploader(lazy {
GeckoViewFetchClient(ApplicationProvider.getApplicationContext()) as Client GeckoViewFetchClient(ApplicationProvider.getApplicationContext())
}) })
// Fenix does not initialize the Glean SDK in tests/debug builds, but this test // Fenix does not initialize the Glean SDK in tests/debug builds, but this test

@ -10,9 +10,9 @@ 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.junit.Ignore
import org.mozilla.fenix.FenixApplication import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
@ -85,7 +85,7 @@ class SettingsBasicsTest {
verifyThemes() verifyThemes()
}.goBack { }.goBack {
}.openAccessibilitySubMenu { }.openAccessibilitySubMenu {
verifyMenuItems() verifyAutomaticFontSizingMenuItems()
}.goBack { }.goBack {
// drill down to submenu // drill down to submenu
} }
@ -181,7 +181,7 @@ class SettingsBasicsTest {
} }
@Test @Test
fun changeAccessibilitySettings() { fun changeAccessibiltySettings() {
// Goes through the settings and changes the default text on a webpage, then verifies if the text has changed. // Goes through the settings and changes the default text on a webpage, then verifies if the text has changed.
val fenixApp = activityIntentTestRule.activity.applicationContext as FenixApplication val fenixApp = activityIntentTestRule.activity.applicationContext as FenixApplication
val webpage = getLoremIpsumAsset(mockWebServer).url val webpage = getLoremIpsumAsset(mockWebServer).url
@ -193,7 +193,8 @@ class SettingsBasicsTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openAccessibilitySubMenu { }.openAccessibilitySubMenu {
verifyMenuItems() clickFontSizingSwitch()
verifyEnabledMenuItems()
changeTextSizeSlider(textSizePercentage) changeTextSizeSlider(textSizePercentage)
verifyTextSizePercentage(textSizePercentage) verifyTextSizePercentage(textSizePercentage)
}.goBack { }.goBack {
@ -201,6 +202,14 @@ class SettingsBasicsTest {
}.openNavigationToolbar { }.openNavigationToolbar {
}.enterURLAndEnterToBrowser(webpage) { }.enterURLAndEnterToBrowser(webpage) {
checkTextSizeOnWebsite(textSizePercentage, fenixApp.components) checkTextSizeOnWebsite(textSizePercentage, fenixApp.components)
}.openTabDrawer {
}.openNewTab {
}.dismiss {
}.openThreeDotMenu {
}.openSettings {
}.openAccessibilitySubMenu {
clickFontSizingSwitch()
verifyMenuItemsAreDisabled()
} }
} }

@ -311,8 +311,10 @@ class TabbedBrowsingTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabButtonShortcutsMenu { }.openTabButtonShortcutsMenu {
}.openNewPrivateTabFromShortcutsMenu { }.openNewPrivateTabFromShortcutsMenu {
verifyHomeScreen() verifyKeyboardVisible()
verifyNavigationToolbar() verifyFocusedNavigationToolbar()
// dismiss search dialog
homeScreen { }.pressBack()
verifyHomePrivateBrowsingButton() verifyHomePrivateBrowsingButton()
verifyHomeMenu() verifyHomeMenu()
verifyHomeWordmark() verifyHomeWordmark()
@ -326,8 +328,10 @@ class TabbedBrowsingTest {
}.openTabButtonShortcutsMenu { }.openTabButtonShortcutsMenu {
}.openTabFromShortcutsMenu { }.openTabFromShortcutsMenu {
verifyHomeScreen() verifyKeyboardVisible()
verifyNavigationToolbar() verifyFocusedNavigationToolbar()
// dismiss search dialog
homeScreen { }.pressBack()
verifyHomeMenu() verifyHomeMenu()
verifyHomeWordmark() verifyHomeWordmark()
verifyTabButton() verifyTabButton()

@ -25,6 +25,7 @@ import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withHint
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
@ -38,6 +39,7 @@ import androidx.test.uiautomator.Until.findObject
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.not
import org.junit.Assert
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.Search import org.mozilla.fenix.components.Search
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
@ -52,6 +54,7 @@ import org.mozilla.fenix.helpers.withBitmapDrawable
*/ */
class HomeScreenRobot { class HomeScreenRobot {
fun verifyNavigationToolbar() = assertNavigationToolbar() fun verifyNavigationToolbar() = assertNavigationToolbar()
fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar()
fun verifyHomeScreen() = assertHomeScreen() fun verifyHomeScreen() = assertHomeScreen()
fun verifyHomePrivateBrowsingButton() = assertHomePrivateBrowsingButton() fun verifyHomePrivateBrowsingButton() = assertHomePrivateBrowsingButton()
fun verifyHomeMenu() = assertHomeMenu() fun verifyHomeMenu() = assertHomeMenu()
@ -64,6 +67,7 @@ class HomeScreenRobot {
fun verifyHomeComponent() = assertHomeComponent() fun verifyHomeComponent() = assertHomeComponent()
fun verifyDefaultSearchEngine(searchEngine: String) = verifySearchEngineIcon(searchEngine) fun verifyDefaultSearchEngine(searchEngine: String) = verifySearchEngineIcon(searchEngine)
fun verifyNoTabsOpened() = assertNoTabsOpened() fun verifyNoTabsOpened() = assertNoTabsOpened()
fun verifyKeyboardVisible() = assertKeyboardVisibility(isExpectedToBeVisible = true)
// First Run elements // First Run elements
fun verifyWelcomeHeader() = assertWelcomeHeader() fun verifyWelcomeHeader() = assertWelcomeHeader()
@ -387,6 +391,10 @@ class HomeScreenRobot {
.perform(click()) .perform(click())
} }
fun pressBack() {
onView(ViewMatchers.isRoot()).perform(ViewActions.pressBack())
}
fun openTabsListThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition { fun openTabsListThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
// tabsListThreeDotButton().perform(click()) // tabsListThreeDotButton().perform(click())
@ -481,6 +489,14 @@ fun homeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) =
Assert.assertEquals(
isExpectedToBeVisible,
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
.executeShellCommand("dumpsys input_method | grep mInputShown")
.contains("mInputShown=true")
)
private fun navigationToolbar() = private fun navigationToolbar() =
onView(allOf(withText("Search or enter address"))) onView(allOf(withText("Search or enter address")))
@ -490,6 +506,10 @@ private fun assertNavigationToolbar() =
onView(allOf(withText("Search or enter address"))) onView(allOf(withText("Search or enter address")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertFocusedNavigationToolbar() =
onView(allOf(withHint("Search or enter address")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertHomeScreen() = onView(ViewMatchers.withResourceName("homeLayout")) private fun assertHomeScreen() = onView(ViewMatchers.withResourceName("homeLayout"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

@ -6,30 +6,32 @@
package org.mozilla.fenix.ui.robots package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import android.view.KeyEvent import android.view.KeyEvent
import android.view.KeyEvent.KEYCODE_DPAD_RIGHT
import android.view.KeyEvent.KEYCODE_DPAD_LEFT
import android.view.KeyEvent.ACTION_DOWN import android.view.KeyEvent.ACTION_DOWN
import android.view.KeyEvent.KEYCODE_DPAD_LEFT
import android.view.KeyEvent.KEYCODE_DPAD_RIGHT
import android.view.View import android.view.View
import android.widget.SeekBar import android.widget.SeekBar
import android.widget.TextView import android.widget.TextView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewAssertion import androidx.test.espresso.ViewAssertion
import org.hamcrest.CoreMatchers.allOf import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
import org.mozilla.fenix.helpers.assertIsEnabled
import org.mozilla.fenix.helpers.isEnabled
import org.mozilla.fenix.ui.robots.SettingsSubMenuAccessibilityRobot.Companion.DECIMAL_CONVERSION import org.mozilla.fenix.ui.robots.SettingsSubMenuAccessibilityRobot.Companion.DECIMAL_CONVERSION
import org.mozilla.fenix.ui.robots.SettingsSubMenuAccessibilityRobot.Companion.MIN_VALUE import org.mozilla.fenix.ui.robots.SettingsSubMenuAccessibilityRobot.Companion.MIN_VALUE
import org.mozilla.fenix.ui.robots.SettingsSubMenuAccessibilityRobot.Companion.STEP_SIZE import org.mozilla.fenix.ui.robots.SettingsSubMenuAccessibilityRobot.Companion.STEP_SIZE
@ -48,7 +50,13 @@ class SettingsSubMenuAccessibilityRobot {
const val TEXT_SIZE = 16f const val TEXT_SIZE = 16f
} }
fun verifyMenuItems() = assertMenuItems() fun verifyAutomaticFontSizingMenuItems() = assertAutomaticFontSizingMenuItems()
fun clickFontSizingSwitch() = toggleFontSizingSwitch()
fun verifyEnabledMenuItems() = assertEnabledMenuItems()
fun verifyMenuItemsAreDisabled() = assertMenuItemsAreDisabled()
fun changeTextSizeSlider(seekBarPercentage: Int) = adjustTextSizeSlider(seekBarPercentage) fun changeTextSizeSlider(seekBarPercentage: Int) = adjustTextSizeSlider(seekBarPercentage)
@ -69,7 +77,22 @@ class SettingsSubMenuAccessibilityRobot {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private fun assertMenuItems() { private fun assertAutomaticFontSizingMenuItems() {
onView(withText("Automatic font sizing"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val strFont = "Font size will match your Android settings. Disable to manage font size here."
onView(withText(strFont))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun toggleFontSizingSwitch() {
// Toggle font size to off
onView(withText("Automatic font sizing"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.perform(click())
}
private fun assertEnabledMenuItems() {
assertFontSize() assertFontSize()
assertSliderBar() assertSliderBar()
} }
@ -77,9 +100,11 @@ private fun assertMenuItems() {
private fun assertFontSize() { private fun assertFontSize() {
val view = onView(withText("Font Size")) val view = onView(withText("Font Size"))
view.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) view.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(isEnabled(true)))
val strFont = "Make text on websites larger or smaller" val strFont = "Make text on websites larger or smaller"
onView(withText(strFont)) onView(withText(strFont))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(isEnabled(true)))
} }
private fun assertSliderBar() { private fun assertSliderBar() {
@ -103,6 +128,20 @@ private fun assertTextSizePercentage(textSize: Int) {
.check(textSizePercentageEquals(textSize)) .check(textSizePercentageEquals(textSize))
} }
private fun assertMenuItemsAreDisabled() {
onView(withText("Font Size")).assertIsEnabled(false)
val strFont = "Make text on websites larger or smaller"
onView(withText(strFont)).assertIsEnabled(false)
onView(withId(org.mozilla.fenix.R.id.sampleText)).assertIsEnabled(false)
onView(withId(org.mozilla.fenix.R.id.seekbar_value)).assertIsEnabled(false)
onView(withId(org.mozilla.fenix.R.id.seekbar)).assertIsEnabled(false)
}
private fun goBackButton() = private fun goBackButton() =
onView(allOf(withContentDescription("Navigate up"))) onView(allOf(withContentDescription("Navigate up")))

@ -14,6 +14,7 @@ import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions
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
@ -532,6 +533,6 @@ private fun assertOpenInAppButton() {
private fun addonsManagerButton() = onView(withText("Add-ons Manager")) private fun addonsManagerButton() = onView(withText("Add-ons Manager"))
private fun clickAddonsManagerButton() { private fun clickAddonsManagerButton() {
onView(withText("Add-ons")).click() onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
addonsManagerButton().click() onView(withText("Add-ons")).check(matches(isCompletelyDisplayed())).click()
} }

@ -47,7 +47,7 @@ object GeckoProvider {
.build() .build()
val settings = context.components.settings val settings = context.components.settings
if (!settings.shouldUseAutoSize()) { if (!settings.shouldUseAutoSize) {
runtimeSettings.automaticFontSizeAdjustment = false runtimeSettings.automaticFontSizeAdjustment = false
val fontSize = settings.fontSizeFactor val fontSize = settings.fontSizeFactor
runtimeSettings.fontSizeFactor = fontSize runtimeSettings.fontSizeFactor = fontSize

@ -47,7 +47,7 @@ object GeckoProvider {
.build() .build()
val settings = context.components.settings val settings = context.components.settings
if (!settings.shouldUseAutoSize()) { if (!settings.shouldUseAutoSize) {
runtimeSettings.automaticFontSizeAdjustment = false runtimeSettings.automaticFontSizeAdjustment = false
val fontSize = settings.fontSizeFactor val fontSize = settings.fontSizeFactor
runtimeSettings.fontSizeFactor = fontSize runtimeSettings.fontSizeFactor = fontSize

@ -57,7 +57,7 @@ object GeckoProvider {
.build() .build()
val settings = context.components.settings val settings = context.components.settings
if (!settings.shouldUseAutoSize()) { if (!settings.shouldUseAutoSize) {
runtimeSettings.automaticFontSizeAdjustment = false runtimeSettings.automaticFontSizeAdjustment = false
val fontSize = settings.fontSizeFactor val fontSize = settings.fontSizeFactor
runtimeSettings.fontSizeFactor = fontSize runtimeSettings.fontSizeFactor = fontSize

@ -229,9 +229,6 @@
<activity android:name=".settings.account.AuthIntentReceiverActivity" <activity android:name=".settings.account.AuthIntentReceiverActivity"
android:exported="false" /> android:exported="false" />
<activity android:name=".settings.about.AboutLibrariesActivity"
android:exported="false" />
<service android:name=".media.MediaService" <service android:name=".media.MediaService"
android:exported="false" /> android:exported="false" />

@ -53,6 +53,10 @@ private const val DEFAULT_VALUE = Int.MAX_VALUE
/** /**
* A dialog that shows [Addon] installation confirmation. * A dialog that shows [Addon] installation confirmation.
*/ */
// We have an extra "Lint" Android Studio linter pass that Android Components
// where the original code came from doesn't. So we tell it to ignore us. Make
// sure to keep up with changes in Android Components though.
@SuppressLint("all")
class PagedAddonInstallationDialogFragment : AppCompatDialogFragment() { class PagedAddonInstallationDialogFragment : AppCompatDialogFragment() {
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
@VisibleForTesting internal var iconJob: Job? = null @VisibleForTesting internal var iconJob: Job? = null

@ -58,6 +58,10 @@ private const val VIEW_HOLDER_TYPE_ADDON = 2
* @property style Indicates how items should look like. * @property style Indicates how items should look like.
*/ */
@Suppress("TooManyFunctions", "LargeClass") @Suppress("TooManyFunctions", "LargeClass")
// We have an extra "Lint" Android Studio linter pass that Android Components
// where the original code came from doesn't. So we tell it to ignore us. Make
// sure to keep up with changes in Android Components though.
@SuppressLint("all")
class PagedAddonsManagerAdapter( class PagedAddonsManagerAdapter(
private val addonCollectionProvider: PagedAddonCollectionProvider, private val addonCollectionProvider: PagedAddonCollectionProvider,
private val addonsManagerDelegate: AddonsManagerAdapterDelegate, private val addonsManagerDelegate: AddonsManagerAdapterDelegate,

@ -21,11 +21,6 @@ object FeatureFlags {
*/ */
val syncedTabsInTabsTray = Config.channel.isNightlyOrDebug val syncedTabsInTabsTray = Config.channel.isNightlyOrDebug
/**
* Enables showing the top frequently visited sites
*/
const val topFrecentSite = true
/** /**
* Shows the grid view settings for the tabs tray. * Shows the grid view settings for the tabs tray.
*/ */
@ -50,4 +45,9 @@ object FeatureFlags {
* Enables ETP cookie purging * Enables ETP cookie purging
*/ */
val etpCookiePurging = Config.channel.isNightlyOrDebug val etpCookiePurging = Config.channel.isNightlyOrDebug
/**
* Returns user to browser on cold start if they have open tabs
*/
val returnToBrowserOnColdStart = Config.channel.isNightlyOrDebug
} }

@ -32,7 +32,6 @@ import mozilla.components.service.glean.config.Configuration
import mozilla.components.service.glean.net.ConceptFetchHttpUploader import mozilla.components.service.glean.net.ConceptFetchHttpUploader
import mozilla.components.support.base.log.Log import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.base.log.sink.AndroidLogSink
import mozilla.components.support.ktx.android.content.isMainProcess import mozilla.components.support.ktx.android.content.isMainProcess
import mozilla.components.support.ktx.android.content.runOnlyInMainProcess import mozilla.components.support.ktx.android.content.runOnlyInMainProcess
import mozilla.components.support.locale.LocaleAwareApplication import mozilla.components.support.locale.LocaleAwareApplication
@ -42,7 +41,6 @@ import mozilla.components.support.utils.logElapsedTime
import mozilla.components.support.webextensions.WebExtensionSupport import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.MetricServiceType import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StorageStatsMetrics import org.mozilla.fenix.perf.StorageStatsMetrics
import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StartupTimeline
@ -114,7 +112,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
setupCrashReporting() setupCrashReporting()
// We want the log messages of all builds to go to Android logcat // We want the log messages of all builds to go to Android logcat
Log.addSink(AndroidLogSink()) Log.addSink(FenixLogSink(logsDebug = Config.channel.isDebug))
} }
@CallSuper @CallSuper

@ -0,0 +1,30 @@
/* 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
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.sink.AndroidLogSink
import mozilla.components.support.base.log.sink.LogSink
/**
* Fenix [LogSink] implementation that writes to Android's log, depending on settings.
*
* @param logsDebug If set to false, removes logging of debug logs.
*/
class FenixLogSink(private val logsDebug: Boolean = true) : LogSink {
private val androidLogSink = AndroidLogSink()
override fun log(
priority: Log.Priority,
tag: String?,
throwable: Throwable?,
message: String?
) {
if (priority == Log.Priority.DEBUG && !logsDebug)
return
androidLogSink.log(priority, tag, throwable, message)
}
}

@ -38,7 +38,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.WebExtensionState import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession
@ -61,7 +61,6 @@ import mozilla.components.support.webextensions.WebExtensionPopupFeature
import org.mozilla.fenix.GleanMetrics.Metrics import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections import org.mozilla.fenix.addons.AddonDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections
import org.mozilla.fenix.browser.UriOpenedObserver
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
@ -122,11 +121,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
private var webExtScope: CoroutineScope? = null private var webExtScope: CoroutineScope? = null
lateinit var themeManager: ThemeManager lateinit var themeManager: ThemeManager
lateinit var browsingModeManager: BrowsingModeManager lateinit var browsingModeManager: BrowsingModeManager
private lateinit var sessionObserver: SessionManager.Observer
private var isVisuallyComplete = false private var isVisuallyComplete = false
private var privateNotificationObserver: PrivateNotificationFeature<PrivateNotificationService>? = null private var privateNotificationObserver: PrivateNotificationFeature<PrivateNotificationService>? =
null
private var isToolbarInflated = false private var isToolbarInflated = false
@ -181,8 +180,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
.attachViewToRunVisualCompletenessQueueLater(WeakReference(rootContainer)) .attachViewToRunVisualCompletenessQueueLater(WeakReference(rootContainer))
} }
sessionObserver = UriOpenedObserver(this)
checkPrivateShortcutEntryPoint(intent) checkPrivateShortcutEntryPoint(intent)
privateNotificationObserver = PrivateNotificationFeature( privateNotificationObserver = PrivateNotificationFeature(
applicationContext, applicationContext,
@ -192,14 +189,18 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
it.start() it.start()
} }
if (isActivityColdStarted(intent, savedInstanceState)) { if (isActivityColdStarted(
externalSourceIntentProcessors.any { intent,
savedInstanceState
) && !externalSourceIntentProcessors.any {
it.process( it.process(
intent, intent,
navHost.navController, navHost.navController,
this.intent this.intent
) )
} }
) {
navigateToBrowserOnColdStart()
} }
Performance.processIntentIfPerformanceTest(intent, this) Performance.processIntentIfPerformanceTest(intent, this)
@ -240,7 +241,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE. StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
} }
protected open fun startupTelemetryOnCreateCalled(safeIntent: SafeIntent, hasSavedInstanceState: Boolean) { protected open fun startupTelemetryOnCreateCalled(
safeIntent: SafeIntent,
hasSavedInstanceState: Boolean
) {
components.appStartupTelemetry.onHomeActivityOnCreate( components.appStartupTelemetry.onHomeActivityOnCreate(
safeIntent, safeIntent,
hasSavedInstanceState, hasSavedInstanceState,
@ -322,10 +326,18 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
final override fun onPause() { final override fun onPause() {
// We should return to the browser if there were normal tabs when we left the app
settings().shouldReturnToBrowser =
components.core.store.state.getNormalOrPrivateTabs(private = false).isNotEmpty()
if (settings().lastKnownMode.isPrivate) { if (settings().lastKnownMode.isPrivate) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} }
// We will remove this when AC code lands to emit a fact on getTopSites in DefaultTopSitesStorage
// https://github.com/mozilla-mobile/android-components/issues/8679
settings().topSitesSize = components.core.topSitesStorage.cachedTopSites.size
super.onPause() super.onPause()
// Diagnostic breadcrumb for "Display already aquired" crash: // Diagnostic breadcrumb for "Display already aquired" crash:
@ -763,6 +775,17 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
} }
open fun navigateToBrowserOnColdStart() {
// Normal tabs + cold start -> Should go back to browser if we had any tabs open when we left last
// except for PBM + Cold Start there won't be any tabs since they're evicted so we never will navigate
if (FeatureFlags.returnToBrowserOnColdStart &&
settings().shouldReturnToBrowser &&
!browsingModeManager.mode.isPrivate
) {
openToBrowser(BrowserDirection.FromGlobal, null)
}
}
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {
base.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { base.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
super.attachBaseContext(base) super.attachBaseContext(base)

@ -0,0 +1,106 @@
/* 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
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.utils.Settings
/**
* [Middleware] to record telemetry in response to [BrowserAction]s.
*
* @property settings reference to the application [Settings].
* @property adsTelemetry reference to [AdsTelemetry] use to record search telemetry.
* @property metrics reference to the configured [MetricController] to record general page load events.
*/
class TelemetryMiddleware(
private val settings: Settings,
private val adsTelemetry: AdsTelemetry,
private val metrics: MetricController
) : Middleware<BrowserState, BrowserAction> {
private val logger = Logger("TelemetryMiddleware")
@VisibleForTesting
internal val redirectChains = mutableMapOf<String, RedirectChain>()
/**
* Utility to collect URLs / load requests in between location changes.
*/
internal class RedirectChain(internal val root: String) {
internal val chain = mutableListOf<String>()
fun add(url: String) {
chain.add(url)
}
}
@Suppress("TooGenericExceptionCaught")
override fun invoke(
context: MiddlewareContext<BrowserState, BrowserAction>,
next: (BrowserAction) -> Unit,
action: BrowserAction
) {
// Pre process actions
when (action) {
is ContentAction.UpdateLoadingStateAction -> {
context.state.findTab(action.sessionId)?.let { tab ->
// Record UriOpened event when a non-private page finishes loading
if (tab.content.loading && !action.loading && !tab.content.private) {
metrics.track(Event.UriOpened)
}
}
}
is ContentAction.UpdateLoadRequestAction -> {
context.state.findTab(action.sessionId)?.let { tab ->
// Collect all load requests in between location changes
if (!redirectChains.containsKey(action.sessionId) && action.loadRequest.url != tab.content.url) {
redirectChains[action.sessionId] = RedirectChain(tab.content.url)
}
redirectChains[action.sessionId]?.add(action.loadRequest.url)
}
}
is ContentAction.UpdateUrlAction -> {
redirectChains[action.sessionId]?.let {
// Record ads telemetry providing all redirects
try {
adsTelemetry.trackAdClickedMetric(it.root, it.chain)
} catch (t: Throwable) {
logger.info("Failed to record search telemetry", t)
} finally {
redirectChains.remove(action.sessionId)
}
}
}
}
next(action)
// Post process actions
when (action) {
is TabListAction.AddTabAction,
is TabListAction.AddMultipleTabsAction,
is TabListAction.RemoveTabAction,
is TabListAction.RemoveAllNormalTabsAction,
is TabListAction.RemoveAllTabsAction,
is TabListAction.RestoreAction -> {
// Update/Persist tabs count whenever it changes
settings.openTabsCount = context.state.normalTabs.count()
}
}
}
}

@ -32,6 +32,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -41,6 +42,7 @@ import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
@ -190,6 +192,28 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
val view = inflater.inflate(R.layout.fragment_browser, container, false) val view = inflater.inflate(R.layout.fragment_browser, container, false)
val activity = activity as HomeActivity val activity = activity as HomeActivity
components = requireComponents
if (customTabSessionId == null) {
// Once tab restoration is complete, if there are no tabs to show in the browser, go home
components.core.store.flowScoped(viewLifecycleOwner) { flow ->
flow.map { state -> state.restoreComplete }
.ifChanged()
.collect { restored ->
if (restored) {
val tabs =
components.core.store.state.getNormalOrPrivateTabs(
activity.browsingModeManager.mode.isPrivate
)
if (tabs.isEmpty()) findNavController().popBackStack(
R.id.homeFragment,
false
)
}
}
}
}
activity.themeManager.applyStatusBarTheme(activity) activity.themeManager.applyStatusBarTheme(activity)
browserFragmentStore = StoreProvider.get(this) { browserFragmentStore = StoreProvider.get(this) {
@ -198,8 +222,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
) )
} }
components = requireComponents
return view return view
} }

@ -11,7 +11,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -86,7 +85,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
val readerModeAction = val readerModeAction =
BrowserToolbar.ToggleButton( BrowserToolbar.ToggleButton(
image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_readermode)!!, image = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode)!!,
imageSelected = imageSelected =
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode_selected)!!, AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode_selected)!!,
contentDescription = requireContext().getString(R.string.browser_menu_read), contentDescription = requireContext().getString(R.string.browser_menu_read),

@ -1,84 +0,0 @@
/* 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.browser
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.session.Session
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
class TelemetrySessionObserver(
private val metrics: MetricController,
private val ads: AdsTelemetry
) : Session.Observer {
private var urlLoading: String? = null
@VisibleForTesting
var redirectChain = mutableListOf<String>()
@VisibleForTesting
var originSessionUrl: String? = null
private val temporaryFix = TemporaryFix()
override fun onLoadingStateChanged(session: Session, loading: Boolean) {
if (loading) {
urlLoading = session.url
} else if (urlLoading != null && !session.private && temporaryFix.shouldSendEvent(session.url)) {
temporaryFix.eventSentFor = session.url
metrics.track(Event.UriOpened)
}
}
/**
* When a link is clicked, record its redirect chain as well as origin url
*/
override fun onLoadRequest(
session: Session,
url: String,
triggeredByRedirect: Boolean,
triggeredByWebContent: Boolean
) {
if (isFirstLinkInRedirectChain(url, session.url)) {
originSessionUrl = session.url
}
if (canStartChain()) {
redirectChain.add(url)
}
}
private fun canStartChain(): Boolean {
return originSessionUrl != null
}
private fun isFirstLinkInRedirectChain(url: String, sessionUrl: String): Boolean {
return originSessionUrl == null && url != sessionUrl
}
/**
* After the redirect chain has finished, check if we encountered an ad on the way and clear
* the stored info for that chain
*/
override fun onUrlChanged(session: Session, url: String) {
ads.trackAdClickedMetric(originSessionUrl, redirectChain)
originSessionUrl = null
redirectChain.clear()
}
/**
* Currently, [Session.Observer.onLoadingStateChanged] is called multiple times the first
* time a new session loads a page. This is inflating our telemetry numbers, so we need to
* handle it, but we will be able to remove this code when [onLoadingStateChanged] has
* been fixed.
*
* See Fenix #3676
* See AC https://github.com/mozilla-mobile/android-components/issues/4795
* TODO remove this class after AC #4795 has been fixed
*/
private class TemporaryFix {
var eventSentFor: String? = null
fun shouldSendEvent(newUrl: String): Boolean = eventSentFor != newUrl
}
}

@ -1,75 +0,0 @@
/* 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.browser
import androidx.annotation.VisibleForTesting
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LifecycleOwner
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.utils.Settings
class UriOpenedObserver(
private val settings: Settings,
private val owner: LifecycleOwner,
private val sessionManager: SessionManager,
metrics: MetricController,
ads: AdsTelemetry
) : SessionManager.Observer {
constructor(activity: FragmentActivity) : this(
activity.applicationContext.settings(),
activity,
activity.components.core.sessionManager,
activity.metrics,
activity.components.core.adsTelemetry
)
@VisibleForTesting
internal val singleSessionObserver = TelemetrySessionObserver(metrics, ads)
init {
sessionManager.register(this, owner)
sessionManager.selectedSession?.register(singleSessionObserver, owner)
}
override fun onSessionSelected(session: Session) {
session.register(singleSessionObserver, owner)
}
private fun saveOpenTabsCount() {
settings.setOpenTabsCount(sessionManager.sessionsOfType(private = false).count())
}
override fun onAllSessionsRemoved() {
saveOpenTabsCount()
sessionManager.sessions.forEach {
it.unregister(singleSessionObserver)
}
}
override fun onSessionAdded(session: Session) {
saveOpenTabsCount()
session.register(singleSessionObserver, owner)
}
override fun onSessionRemoved(session: Session) {
saveOpenTabsCount()
session.unregister(singleSessionObserver)
}
override fun onSessionsRestored() {
saveOpenTabsCount()
sessionManager.sessions.forEach {
it.register(singleSessionObserver, owner)
}
}
}

@ -7,7 +7,7 @@ package org.mozilla.fenix.browser.readermode
import android.view.View import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.RadioButton import android.widget.RadioButton
import androidx.core.content.ContextCompat import androidx.appcompat.content.res.AppCompatResources
import mozilla.components.feature.readerview.ReaderViewFeature import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -54,7 +54,7 @@ class DefaultReaderModeController(
findViewById<Button>(it) findViewById<Button>(it)
}.forEach { }.forEach {
it.setTextColor( it.setTextColor(
ContextCompat.getColorStateList( AppCompatResources.getColorStateList(
context, context,
R.color.readerview_private_button_color R.color.readerview_private_button_color
) )
@ -68,7 +68,7 @@ class DefaultReaderModeController(
findViewById<RadioButton>(it) findViewById<RadioButton>(it)
}.forEach { }.forEach {
it.setTextColor( it.setTextColor(
ContextCompat.getColorStateList( AppCompatResources.getColorStateList(
context, context,
R.color.readerview_private_radio_color R.color.readerview_private_radio_color
) )

@ -7,6 +7,7 @@ package org.mozilla.fenix.components
import GeckoProvider import GeckoProvider
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build
import android.os.StrictMode import android.os.StrictMode
import io.sentry.Sentry import io.sentry.Sentry
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -21,6 +22,7 @@ import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.engine.EngineMiddleware import mozilla.components.browser.session.engine.EngineMiddleware
import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.session.undo.UndoMiddleware import mozilla.components.browser.session.undo.UndoMiddleware
import mozilla.components.browser.state.action.RestoreCompleteAction
import mozilla.components.browser.state.action.RecentlyClosedAction import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
@ -61,6 +63,7 @@ import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.StrictModeManager import org.mozilla.fenix.StrictModeManager
import org.mozilla.fenix.TelemetryMiddleware
import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -90,13 +93,14 @@ class Core(
val engine: Engine by lazy { val engine: Engine by lazy {
val defaultSettings = DefaultSettings( val defaultSettings = DefaultSettings(
requestInterceptor = AppRequestInterceptor(context), requestInterceptor = AppRequestInterceptor(context),
remoteDebuggingEnabled = context.settings().isRemoteDebuggingEnabled, remoteDebuggingEnabled = context.settings().isRemoteDebuggingEnabled &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M,
testingModeEnabled = false, testingModeEnabled = false,
trackingProtectionPolicy = trackingProtectionPolicyFactory.createTrackingProtectionPolicy(), trackingProtectionPolicy = trackingProtectionPolicyFactory.createTrackingProtectionPolicy(),
historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage), historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage),
preferredColorScheme = getPreferredColorScheme(), preferredColorScheme = getPreferredColorScheme(),
automaticFontSizeAdjustment = context.settings().shouldUseAutoSize(), automaticFontSizeAdjustment = context.settings().shouldUseAutoSize,
fontInflationEnabled = context.settings().shouldUseAutoSize(), fontInflationEnabled = context.settings().shouldUseAutoSize,
suspendMediaWhenInactive = false, suspendMediaWhenInactive = false,
forceUserScalableContent = context.settings().forceEnableZoom, forceUserScalableContent = context.settings().forceEnableZoom,
loginAutofillEnabled = context.settings().shouldAutofillLogins loginAutofillEnabled = context.settings().shouldAutofillLogins
@ -153,6 +157,11 @@ class Core(
MediaMiddleware(context, MediaService::class.java), MediaMiddleware(context, MediaService::class.java),
DownloadMiddleware(context, DownloadService::class.java), DownloadMiddleware(context, DownloadService::class.java),
ReaderViewMiddleware(), ReaderViewMiddleware(),
TelemetryMiddleware(
context.settings(),
adsTelemetry,
metrics
),
ThumbnailsMiddleware(thumbnailStorage), ThumbnailsMiddleware(thumbnailStorage),
UndoMiddleware(::lookupSessionManager, context.getUndoDelay()) UndoMiddleware(::lookupSessionManager, context.getUndoDelay())
) + EngineMiddleware.create(engine, ::findSessionById) ) + EngineMiddleware.create(engine, ::findSessionById)
@ -231,6 +240,8 @@ class Core(
} }
} }
} }
store.dispatch(RestoreCompleteAction)
} }
WebNotificationFeature( WebNotificationFeature(
@ -247,12 +258,16 @@ class Core(
BrowserIcons(context, client) BrowserIcons(context, client)
} }
val metrics by lazy {
context.components.analytics.metrics
}
val adsTelemetry by lazy { val adsTelemetry by lazy {
AdsTelemetry(context.components.analytics.metrics) AdsTelemetry(metrics)
} }
val searchTelemetry by lazy { val searchTelemetry by lazy {
InContentTelemetry(context.components.analytics.metrics) InContentTelemetry(metrics)
} }
/** /**

@ -10,7 +10,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.search.SearchEngineManager import mozilla.components.browser.search.SearchEngineManager
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
/** /**
@ -30,10 +29,7 @@ class Search(private val context: Context) {
).apply { ).apply {
registerForLocaleUpdates(context) registerForLocaleUpdates(context)
GlobalScope.launch { GlobalScope.launch {
defaultSearchEngine = getDefaultSearchEngineAsync( defaultSearchEngine = provider.getDefaultEngine(context)
context,
context.settings().defaultSearchEngineName
)
} }
} }
} }

@ -15,6 +15,7 @@ import mozilla.components.feature.downloads.DownloadsUseCases
import mozilla.components.feature.pwa.WebAppShortcutManager import mozilla.components.feature.pwa.WebAppShortcutManager
import mozilla.components.feature.pwa.WebAppUseCases import mozilla.components.feature.pwa.WebAppUseCases
import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.search.SearchUseCases
import mozilla.components.browser.search.ext.toDefaultSearchEngineProvider
import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.session.SettingsUseCases import mozilla.components.feature.session.SettingsUseCases
import mozilla.components.feature.session.TrackingProtectionUseCases import mozilla.components.feature.session.TrackingProtectionUseCases
@ -51,7 +52,14 @@ class UseCases(
/** /**
* Use cases that provide search engine integration. * Use cases that provide search engine integration.
*/ */
val searchUseCases by lazy { SearchUseCases(context, store, searchEngineManager, sessionManager) } val searchUseCases by lazy {
SearchUseCases(
context,
store,
searchEngineManager.toDefaultSearchEngineProvider(context),
sessionManager
)
}
/** /**
* Use cases that provide settings management. * Use cases that provide settings management.

@ -194,6 +194,8 @@ sealed class Event {
object MasterPasswordMigrationSuccess : Event() object MasterPasswordMigrationSuccess : Event()
object MasterPasswordMigrationDisplayed : Event() object MasterPasswordMigrationDisplayed : Event()
object TabSettingsOpened : Event()
// Interaction events with extras // Interaction events with extras
data class TopSiteSwipeCarousel(val page: Int) : Event() { data class TopSiteSwipeCarousel(val page: Int) : Event() {

@ -48,6 +48,7 @@ import org.mozilla.fenix.GleanMetrics.SearchWidgetCfr
import org.mozilla.fenix.GleanMetrics.SyncAccount import org.mozilla.fenix.GleanMetrics.SyncAccount
import org.mozilla.fenix.GleanMetrics.SyncAuth import org.mozilla.fenix.GleanMetrics.SyncAuth
import org.mozilla.fenix.GleanMetrics.Tab import org.mozilla.fenix.GleanMetrics.Tab
import org.mozilla.fenix.GleanMetrics.Tabs
import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.GleanMetrics.Tip import org.mozilla.fenix.GleanMetrics.Tip
import org.mozilla.fenix.GleanMetrics.ToolbarSettings import org.mozilla.fenix.GleanMetrics.ToolbarSettings
@ -705,6 +706,9 @@ private val Event.wrapper: EventWrapper<*>?
Event.MasterPasswordMigrationSuccess -> EventWrapper<NoExtraKeys>( Event.MasterPasswordMigrationSuccess -> EventWrapper<NoExtraKeys>(
{ MasterPassword.migration.record(it) } { MasterPassword.migration.record(it) }
) )
Event.TabSettingsOpened -> EventWrapper<NoExtraKeys>(
{ Tabs.settingOpened.record(it) }
)
// Don't record other events in Glean: // Don't record other events in Glean:
is Event.AddBookmark -> null is Event.AddBookmark -> null
@ -719,7 +723,11 @@ private val Event.wrapper: EventWrapper<*>?
is Event.ChangedToDefaultBrowser -> null is Event.ChangedToDefaultBrowser -> null
} }
class GleanMetricsService(private val context: Context) : MetricsService { class GleanMetricsService(
private val context: Context,
private val browsersCache: BrowsersCache = BrowsersCache,
private val mozillaProductDetector: MozillaProductDetector = MozillaProductDetector
) : MetricsService {
override val type = MetricServiceType.Data override val type = MetricServiceType.Data
private val logger = Logger("GleanMetricsService") private val logger = Logger("GleanMetricsService")
@ -754,12 +762,12 @@ class GleanMetricsService(private val context: Context) : MetricsService {
internal fun setStartupMetrics() { internal fun setStartupMetrics() {
setPreferenceMetrics() setPreferenceMetrics()
Metrics.apply { with(Metrics) {
defaultBrowser.set(BrowsersCache.all(context).isDefaultBrowser) defaultBrowser.set(browsersCache.all(context).isDefaultBrowser)
MozillaProductDetector.getMozillaBrowserDefault(context)?.also { mozillaProductDetector.getMozillaBrowserDefault(context)?.also {
defaultMozBrowser.set(it) defaultMozBrowser.set(it)
} }
mozillaProducts.set(MozillaProductDetector.getInstalledMozillaProducts(context)) mozillaProducts.set(mozillaProductDetector.getInstalledMozillaProducts(context))
adjustCampaign.set(context.settings().adjustCampaignId) adjustCampaign.set(context.settings().adjustCampaignId)
adjustAdGroup.set(context.settings().adjustAdGroup) adjustAdGroup.set(context.settings().adjustAdGroup)
@ -786,6 +794,9 @@ class GleanMetricsService(private val context: Context) : MetricsService {
ToolbarPosition.TOP -> Event.ToolbarPositionChanged.Position.TOP.name ToolbarPosition.TOP -> Event.ToolbarPositionChanged.Position.TOP.name
} }
) )
tabViewSetting.set(context.settings().getTabViewPingString())
closeTabSetting.set(context.settings().getTabTimeoutPingString())
} }
SearchDefaultEngine.apply { SearchDefaultEngine.apply {
@ -808,7 +819,7 @@ class GleanMetricsService(private val context: Context) : MetricsService {
// We purposefully make all of our preferences the string_list format to make data analysis // We purposefully make all of our preferences the string_list format to make data analysis
// simpler. While it makes things like booleans a bit more complicated, it means all our // simpler. While it makes things like booleans a bit more complicated, it means all our
// preferences can be analyzed with the same dashboard and compared. // preferences can be analyzed with the same dashboard and compared.
Preferences.apply { with(Preferences) {
showSearchSuggestions.set(context.settings().shouldShowSearchSuggestions.toStringList()) showSearchSuggestions.set(context.settings().shouldShowSearchSuggestions.toStringList())
remoteDebugging.set(context.settings().isRemoteDebuggingEnabled.toStringList()) remoteDebugging.set(context.settings().isRemoteDebuggingEnabled.toStringList())
telemetry.set(context.settings().isTelemetryEnabled.toStringList()) telemetry.set(context.settings().isTelemetryEnabled.toStringList())
@ -859,7 +870,9 @@ class GleanMetricsService(private val context: Context) : MetricsService {
val accessibilitySelection = mutableListOf<String>() val accessibilitySelection = mutableListOf<String>()
if (context.settings().switchServiceIsEnabled) { accessibilitySelection.add("switch") } if (context.settings().switchServiceIsEnabled) {
accessibilitySelection.add("switch")
}
if (context.settings().touchExplorationIsEnabled) { if (context.settings().touchExplorationIsEnabled) {
accessibilitySelection.add("touch exploration") accessibilitySelection.add("touch exploration")

@ -33,7 +33,7 @@ import java.util.Locale
open class FenixSearchEngineProvider( open class FenixSearchEngineProvider(
private val context: Context private val context: Context
) : SearchEngineProvider, CoroutineScope by CoroutineScope(Job() + Dispatchers.IO) { ) : SearchEngineProvider, CoroutineScope by CoroutineScope(Job() + Dispatchers.IO) {
private val shouldMockMLS = Config.channel.isDebug || BuildConfig.MLS_TOKEN.isNullOrEmpty() private val shouldMockMLS = Config.channel.isDebug || BuildConfig.MLS_TOKEN.isEmpty()
private val locationService: LocationService = if (shouldMockMLS) { private val locationService: LocationService = if (shouldMockMLS) {
LocationService.dummy() LocationService.dummy()
} else { } else {
@ -55,8 +55,12 @@ open class FenixSearchEngineProvider(
open val localizationProvider: SearchLocalizationProvider = open val localizationProvider: SearchLocalizationProvider =
RegionSearchLocalizationProvider(locationService) RegionSearchLocalizationProvider(locationService)
/**
* Unfiltered list of search engines based on locale.
*/
open var baseSearchEngines = async { open var baseSearchEngines = async {
AssetsSearchEngineProvider(localizationProvider).loadSearchEngines(context) AssetsSearchEngineProvider(localizationProvider)
.loadSearchEngines(context)
} }
private val loadedRegion = async { localizationProvider.determineRegion() } private val loadedRegion = async { localizationProvider.determineRegion() }
@ -72,9 +76,13 @@ open class FenixSearchEngineProvider(
open val fallbackEngines = async { fallBackProvider.loadSearchEngines(context) } open val fallbackEngines = async { fallBackProvider.loadSearchEngines(context) }
private val fallbackRegion = async { fallbackLocationService.determineRegion() } private val fallbackRegion = async { fallbackLocationService.determineRegion() }
/**
* Default bundled search engines based on locale.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
open val bundledSearchEngines = async { open val bundledSearchEngines = async {
val defaultEngineIdentifiers = baseSearchEngines.await().list.map { it.identifier }.toSet() val defaultEngineIdentifiers =
baseSearchEngines.await().list.map { it.identifier }.toSet()
AssetsSearchEngineProvider( AssetsSearchEngineProvider(
localizationProvider, localizationProvider,
filters = listOf(object : SearchEngineFilter { filters = listOf(object : SearchEngineFilter {
@ -87,12 +95,15 @@ open class FenixSearchEngineProvider(
).loadSearchEngines(context) ).loadSearchEngines(context)
} }
/**
* Search engines that have been manually added by a user.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
open var customSearchEngines = async { open var customSearchEngines = async {
CustomSearchEngineProvider().loadSearchEngines(context) CustomSearchEngineProvider().loadSearchEngines(context)
} }
private var loadedSearchEngines = refreshAsync(baseSearchEngines) private var loadedSearchEngines = refreshInstalledEngineListAsync(baseSearchEngines)
// https://github.com/mozilla-mobile/fenix/issues/9935 // https://github.com/mozilla-mobile/fenix/issues/9935
// Create new getter that will return the fallback SearchEngineList if // Create new getter that will return the fallback SearchEngineList if
@ -102,29 +113,45 @@ open class FenixSearchEngineProvider(
if (isRegionCachedByLocationService || shouldMockMLS) { if (isRegionCachedByLocationService || shouldMockMLS) {
loadedSearchEngines loadedSearchEngines
} else { } else {
refreshAsync(fallbackEngines) refreshInstalledEngineListAsync(fallbackEngines)
} }
fun getDefaultEngine(context: Context): SearchEngine { fun getDefaultEngine(context: Context): SearchEngine {
val engines = installedSearchEngines(context) val engines = installedSearchEngines(context)
val selectedName = context.settings().defaultSearchEngineName val selectedName = context.settings().defaultSearchEngineName
return engines.list.find { it.name == selectedName } ?: engines.default ?: engines.list.first() return engines.list.find { it.name == selectedName }
?: engines.default
?: engines.list.first()
}
// We should only be setting the default search engine here
fun setDefaultEngine(context: Context, id: String) {
val engines = installedSearchEngines(context)
val newDefault = engines.list.find { it.name == id }
?: engines.default
?: engines.list.first()
context.settings().defaultSearchEngineName = newDefault.name
context.components.search.searchEngineManager.defaultSearchEngine = newDefault
} }
/** /**
* @return a list of all SearchEngines that are currently active. These are the engines that * @return a list of all SearchEngines that are currently active. These are the engines that
* are readily available throughout the app. * are readily available throughout the app. Includes all installed engines, both
* default and custom
*/ */
fun installedSearchEngines(context: Context): SearchEngineList = runBlocking { fun installedSearchEngines(context: Context): SearchEngineList = runBlocking {
val installedIdentifiers = installedSearchEngineIdentifiers(context) val installedIdentifiers = installedSearchEngineIdentifiers(context)
val engineList = searchEngines.await() val defaultList = searchEngines.await()
engineList.copy( defaultList.copy(
list = engineList.list.filter { list = defaultList.list.filter {
installedIdentifiers.contains(it.identifier) installedIdentifiers.contains(it.identifier)
}.sortedBy { it.name.toLowerCase(Locale.getDefault()) }, }.sortedBy {
default = engineList.default?.let { it.name.toLowerCase(Locale.getDefault())
},
default = defaultList.default?.let {
if (installedIdentifiers.contains(it.identifier)) { if (installedIdentifiers.contains(it.identifier)) {
it it
} else { } else {
@ -142,14 +169,20 @@ open class FenixSearchEngineProvider(
val installedIdentifiers = installedSearchEngineIdentifiers(context) val installedIdentifiers = installedSearchEngineIdentifiers(context)
val engineList = loadedSearchEngines.await() val engineList = loadedSearchEngines.await()
engineList.copy(list = engineList.list.filterNot { installedIdentifiers.contains(it.identifier) }) return@runBlocking engineList.copy(
list = engineList.list.filterNot { installedIdentifiers.contains(it.identifier) }
)
} }
override suspend fun loadSearchEngines(context: Context): SearchEngineList { override suspend fun loadSearchEngines(context: Context): SearchEngineList {
return installedSearchEngines(context) return installedSearchEngines(context)
} }
fun installSearchEngine(context: Context, searchEngine: SearchEngine, isCustom: Boolean = false) = runBlocking { fun installSearchEngine(
context: Context,
searchEngine: SearchEngine,
isCustom: Boolean = false
) = runBlocking {
if (isCustom) { if (isCustom) {
val searchUrl = searchEngine.getSearchTemplate() val searchUrl = searchEngine.getSearchTemplate()
CustomSearchEngineStore.addSearchEngine(context, searchEngine.name, searchUrl) CustomSearchEngineStore.addSearchEngine(context, searchEngine.name, searchUrl)
@ -158,25 +191,34 @@ open class FenixSearchEngineProvider(
val installedIdentifiers = installedSearchEngineIdentifiers(context).toMutableSet() val installedIdentifiers = installedSearchEngineIdentifiers(context).toMutableSet()
installedIdentifiers.add(searchEngine.identifier) installedIdentifiers.add(searchEngine.identifier)
prefs(context).edit() prefs(context).edit()
.putStringSet(localeAwareInstalledEnginesKey(), installedIdentifiers).apply() .putStringSet(
localeAwareInstalledEnginesKey(), installedIdentifiers
).apply()
} }
} }
fun uninstallSearchEngine(context: Context, searchEngine: SearchEngine, isCustom: Boolean = false) = runBlocking { fun uninstallSearchEngine(
context: Context,
searchEngine: SearchEngine,
isCustom: Boolean = false
) = runBlocking {
if (isCustom) { if (isCustom) {
CustomSearchEngineStore.removeSearchEngine(context, searchEngine.identifier) CustomSearchEngineStore.removeSearchEngine(context, searchEngine.identifier)
reload() reload()
} else { } else {
val installedIdentifiers = installedSearchEngineIdentifiers(context).toMutableSet() val installedIdentifiers = installedSearchEngineIdentifiers(context).toMutableSet()
installedIdentifiers.remove(searchEngine.identifier) installedIdentifiers.remove(searchEngine.identifier)
prefs(context).edit().putStringSet(localeAwareInstalledEnginesKey(), installedIdentifiers).apply() prefs(context).edit().putStringSet(
localeAwareInstalledEnginesKey(),
installedIdentifiers
).apply()
} }
} }
fun reload() { fun reload() {
launch { launch {
customSearchEngines = async { CustomSearchEngineProvider().loadSearchEngines(context) } customSearchEngines = async { CustomSearchEngineProvider().loadSearchEngines(context) }
loadedSearchEngines = refreshAsync(baseSearchEngines) loadedSearchEngines = refreshInstalledEngineListAsync(baseSearchEngines)
} }
} }
@ -184,16 +226,19 @@ open class FenixSearchEngineProvider(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
open fun updateBaseSearchEngines() { open fun updateBaseSearchEngines() {
baseSearchEngines = async { baseSearchEngines = async {
AssetsSearchEngineProvider(localizationProvider).loadSearchEngines(context) AssetsSearchEngineProvider(localizationProvider)
.loadSearchEngines(context)
} }
} }
private fun refreshAsync(baseList: Deferred<SearchEngineList>) = async { private fun refreshInstalledEngineListAsync(
val engineList = baseList.await() engines: Deferred<SearchEngineList>
): Deferred<SearchEngineList> = async {
val engineList = engines.await()
val bundledList = bundledSearchEngines.await().list val bundledList = bundledSearchEngines.await().list
val customList = customSearchEngines.await().list val customList = customSearchEngines.await().list
engineList.copy(list = engineList.list + bundledList + customList) return@async engineList.copy(list = engineList.list + bundledList + customList)
} }
private fun prefs(context: Context) = context.getSharedPreferences( private fun prefs(context: Context) = context.getSharedPreferences(
@ -208,8 +253,11 @@ open class FenixSearchEngineProvider(
if (!prefs.contains(installedEnginesKey)) { if (!prefs.contains(installedEnginesKey)) {
val searchEngines = val searchEngines =
if (isRegionCachedByLocationService) baseSearchEngines if (isRegionCachedByLocationService) {
else fallbackEngines baseSearchEngines
} else {
fallbackEngines
}
val defaultSet = searchEngines.await() val defaultSet = searchEngines.await()
.list .list
@ -219,9 +267,12 @@ open class FenixSearchEngineProvider(
prefs.edit().putStringSet(installedEnginesKey, defaultSet).apply() prefs.edit().putStringSet(installedEnginesKey, defaultSet).apply()
} }
val installedIdentifiers = prefs(context).getStringSet(installedEnginesKey, setOf()) ?: setOf() val installedIdentifiers: Set<String> =
prefs(context).getStringSet(installedEnginesKey, setOf()) ?: setOf()
val customEngineIdentifiers =
customSearchEngines.await().list.map { it.identifier }.toSet()
val customEngineIdentifiers = customSearchEngines.await().list.map { it.identifier }.toSet()
return installedIdentifiers + customEngineIdentifiers return installedIdentifiers + customEngineIdentifiers
} }
@ -251,6 +302,5 @@ open class FenixSearchEngineProvider(
val BUNDLED_SEARCH_ENGINES = listOf("reddit", "youtube") val BUNDLED_SEARCH_ENGINES = listOf("reddit", "youtube")
const val PREF_FILE_SEARCH_ENGINES = "fenix-search-engine-provider" const val PREF_FILE_SEARCH_ENGINES = "fenix-search-engine-provider"
const val INSTALLED_ENGINES_KEY = "fenix-installed-search-engines" const val INSTALLED_ENGINES_KEY = "fenix-installed-search-engines"
const val CURRENT_LOCALE_KEY = "fenix-current-locale"
} }
} }

@ -9,7 +9,7 @@ import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
@ -73,7 +73,7 @@ class MasterPasswordTipProvider(
title = context.getString(R.string.mp_homescreen_tip_title), title = context.getString(R.string.mp_homescreen_tip_title),
description = context.getString(R.string.mp_homescreen_tip_message), description = context.getString(R.string.mp_homescreen_tip_message),
learnMoreURL = null, learnMoreURL = null,
titleDrawable = ContextCompat.getDrawable(context, R.drawable.ic_login) titleDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_login)
) )
private fun showMasterPasswordMigration() { private fun showMasterPasswordMigration() {

@ -124,7 +124,7 @@ class DefaultBrowserToolbarController(
} }
is TabCounterMenu.Item.NewTab -> { is TabCounterMenu.Item.NewTab -> {
activity.browsingModeManager.mode = item.mode activity.browsingModeManager.mode = item.mode
navController.popBackStack(R.id.homeFragment, false) navController.navigate(BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
} }
} }
} }

@ -18,11 +18,13 @@ import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
import mozilla.components.browser.menu.item.BrowserMenuImageSwitch import mozilla.components.browser.menu.item.BrowserMenuImageSwitch
import mozilla.components.browser.menu.item.BrowserMenuImageText import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.menu.item.BrowserMenuItemToolbar import mozilla.components.browser.menu.item.BrowserMenuItemToolbar
import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.storage.BookmarksStorage import mozilla.components.concept.storage.BookmarksStorage
import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature
import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -188,6 +190,7 @@ class DefaultToolbarMenu(
settings, settings,
if (shouldDeleteDataOnQuit) deleteDataOnQuit else null, if (shouldDeleteDataOnQuit) deleteDataOnQuit else null,
BrowserMenuDivider(), BrowserMenuDivider(),
reportSiteIssuePlaceholder,
findInPage, findInPage,
addToTopSites, addToTopSites,
addToHomescreen.apply { visible = ::canAddToHomescreen }, addToHomescreen.apply { visible = ::canAddToHomescreen },
@ -283,6 +286,10 @@ class DefaultToolbarMenu(
onItemTapped.invoke(ToolbarMenu.Item.FindInPage) onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
} }
private val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem(
id = WebCompatReporterFeature.WEBCOMPAT_REPORTER_EXTENSION_ID
)
private val saveToCollection = BrowserMenuImageText( private val saveToCollection = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_save_to_collection_2), label = context.getString(R.string.browser_menu_save_to_collection_2),
imageResource = R.drawable.ic_tab_collection, imageResource = R.drawable.ic_tab_collection,

@ -39,7 +39,7 @@ class TabCounter @JvmOverloads constructor(
counter_root.contentDescription = if (count == 1) { counter_root.contentDescription = if (count == 1) {
context?.getString(R.string.open_tab_tray_single) context?.getString(R.string.open_tab_tray_single)
} else { } else {
context?.getString(R.string.open_tab_tray_plural, count.toString()) String.format(context.getString(R.string.open_tab_tray_plural), count.toString())
} }
} }
@ -191,7 +191,8 @@ class TabCounter @JvmOverloads constructor(
ONE_DIGIT_SIZE_RATIO ONE_DIGIT_SIZE_RATIO
} }
val counterBoxWidth = context.resources.getDimensionPixelSize(R.dimen.tab_counter_box_width_height) val counterBoxWidth =
context.resources.getDimensionPixelSize(R.dimen.tab_counter_box_width_height)
val textSize = newRatio * counterBoxWidth val textSize = newRatio * counterBoxWidth
counter_text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) counter_text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
counter_text.setTypeface(null, Typeface.BOLD) counter_text.setTypeface(null, Typeface.BOLD)

@ -5,10 +5,9 @@
package org.mozilla.fenix.components.toolbar package org.mozilla.fenix.components.toolbar
import android.content.Context import android.content.Context
import android.content.res.Configuration
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieDrawable
import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.display.DisplayToolbar import mozilla.components.browser.toolbar.display.DisplayToolbar
@ -92,45 +91,50 @@ class DefaultToolbarIntegration(
toolbar.display.menuBuilder = toolbarMenu.menuBuilder toolbar.display.menuBuilder = toolbarMenu.menuBuilder
toolbar.private = isPrivate toolbar.private = isPrivate
val task = LottieCompositionFactory val drawable =
.fromRawRes( if (isPrivate) AppCompatResources.getDrawable(
context, context,
ThemeManager.resolveAttribute(R.attr.shieldLottieFile, context) R.drawable.shield_dark
) ) else when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
task.addListener { result -> Configuration.UI_MODE_NIGHT_UNDEFINED, // We assume light here per Android doc's recommendation
val lottieDrawable = LottieDrawable() Configuration.UI_MODE_NIGHT_NO -> {
lottieDrawable.composition = result AppCompatResources.getDrawable(context, R.drawable.shield_light)
toolbar.display.indicators =
if (context.settings().shouldUseTrackingProtection) {
listOf(
DisplayToolbar.Indicators.TRACKING_PROTECTION,
DisplayToolbar.Indicators.SECURITY,
DisplayToolbar.Indicators.EMPTY
)
} else {
listOf(
DisplayToolbar.Indicators.SECURITY,
DisplayToolbar.Indicators.EMPTY
)
} }
Configuration.UI_MODE_NIGHT_YES -> {
AppCompatResources.getDrawable(context, R.drawable.shield_dark)
}
else -> null
}
toolbar.display.displayIndicatorSeparator = toolbar.display.indicators =
context.settings().shouldUseTrackingProtection if (context.settings().shouldUseTrackingProtection) {
listOf(
toolbar.display.icons = toolbar.display.icons.copy( DisplayToolbar.Indicators.TRACKING_PROTECTION,
emptyIcon = null, DisplayToolbar.Indicators.SECURITY,
trackingProtectionTrackersBlocked = lottieDrawable, DisplayToolbar.Indicators.EMPTY
trackingProtectionNothingBlocked = AppCompatResources.getDrawable( )
context, } else {
R.drawable.ic_tracking_protection_enabled listOf(
)!!, DisplayToolbar.Indicators.SECURITY,
trackingProtectionException = AppCompatResources.getDrawable( DisplayToolbar.Indicators.EMPTY
context, )
R.drawable.ic_tracking_protection_disabled }
)!!
) toolbar.display.displayIndicatorSeparator =
} context.settings().shouldUseTrackingProtection
toolbar.display.icons = toolbar.display.icons.copy(
emptyIcon = null,
trackingProtectionTrackersBlocked = drawable!!,
trackingProtectionNothingBlocked = AppCompatResources.getDrawable(
context,
R.drawable.ic_tracking_protection_enabled
)!!,
trackingProtectionException = AppCompatResources.getDrawable(
context,
R.drawable.ic_tracking_protection_disabled
)!!
)
val tabsAction = TabCounterToolbarButton( val tabsAction = TabCounterToolbarButton(
lifecycleOwner, lifecycleOwner,

@ -25,6 +25,7 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getStringWithArgSafe import org.mozilla.fenix.ext.getStringWithArgSafe
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import java.util.Locale
/** /**
* Builds the toolbar object used with the 3-dot menu in the custom tab browser fragment. * Builds the toolbar object used with the 3-dot menu in the custom tab browser fragment.
@ -121,7 +122,11 @@ class CustomTabToolbarMenu(
BrowserMenuDivider(), BrowserMenuDivider(),
menuToolbar menuToolbar
) )
if (shouldReverseItems) { menuItems.reversed() } else { menuItems } if (shouldReverseItems) {
menuItems.reversed()
} else {
menuItems
}
} }
private val desktopMode = BrowserMenuImageSwitch( private val desktopMode = BrowserMenuImageSwitch(
@ -161,7 +166,8 @@ class CustomTabToolbarMenu(
} }
private val poweredBy = BrowserMenuCategory( private val poweredBy = BrowserMenuCategory(
label = context.getStringWithArgSafe(R.string.browser_menu_powered_by, appName).toUpperCase(), label = context.getStringWithArgSafe(R.string.browser_menu_powered_by, appName)
.toUpperCase(Locale.getDefault()),
textSize = CAPTION_TEXT_SIZE, textSize = CAPTION_TEXT_SIZE,
textColorResource = primaryTextColor(), textColorResource = primaryTextColor(),
textStyle = Typeface.NORMAL textStyle = Typeface.NORMAL

@ -46,6 +46,10 @@ open class ExternalAppBrowserActivity : HomeActivity() {
) )
} }
override fun navigateToBrowserOnColdStart() {
// No-op for external app
}
override fun getNavDirections( override fun getNavDirections(
from: BrowserDirection, from: BrowserDirection,
customTabSessionId: String? customTabSessionId: String?

@ -24,8 +24,10 @@ import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.ext.putWebAppManifest import mozilla.components.feature.pwa.ext.putWebAppManifest
import mozilla.components.feature.pwa.ext.toCustomTabConfig import mozilla.components.feature.pwa.ext.toCustomTabConfig
import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SessionUseCases
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.utils.SafeIntent import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent import mozilla.components.support.utils.toSafeIntent
import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import org.mozilla.fenix.R import org.mozilla.fenix.R
import java.io.File import java.io.File
@ -40,6 +42,7 @@ class FennecWebAppIntentProcessor(
private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase, private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase,
private val storage: ManifestStorage private val storage: ManifestStorage
) : IntentProcessor { ) : IntentProcessor {
val logger = Logger("FennecWebAppIntentProcessor")
/** /**
* Returns true if this intent should launch a progressive web app created in Fennec. * Returns true if this intent should launch a progressive web app created in Fennec.
@ -113,6 +116,10 @@ class FennecWebAppIntentProcessor(
WebAppManifestParser().parse(manifestField).getOrNull() WebAppManifestParser().parse(manifestField).getOrNull()
} catch (e: IOException) { } catch (e: IOException) {
logger.error("Failed to parse web app manifest due to IOException", e)
null
} catch (e: JSONException) {
logger.error("Failed to parse web app manifest due to JSONException", e)
null null
} }
} }

@ -16,6 +16,7 @@ import kotlinx.android.synthetic.main.fragment_exceptions.view.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import mozilla.components.feature.logins.exceptions.LoginException
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.StoreProvider
@ -61,7 +62,7 @@ class LoginExceptionsFragment : Fragment() {
private fun subscribeToLoginExceptions() { private fun subscribeToLoginExceptions() {
requireComponents.core.loginExceptionStorage.getLoginExceptions().asLiveData() requireComponents.core.loginExceptionStorage.getLoginExceptions().asLiveData()
.observe(viewLifecycleOwner) { exceptions -> .observe<List<LoginException>>(viewLifecycleOwner) { exceptions ->
exceptionsStore.dispatch(ExceptionsFragmentAction.Change(exceptions)) exceptionsStore.dispatch(ExceptionsFragmentAction.Change(exceptions))
} }
} }

@ -35,5 +35,5 @@ fun List<TabCollection>.getDefaultCollectionNumber(): Int {
.map { it.title } .map { it.title }
.filter { it.matches(Regex("Collection\\s\\d+")) } .filter { it.matches(Regex("Collection\\s\\d+")) }
.map { Integer.valueOf(it.split(" ")[DefaultCollectionCreationController.DEFAULT_COLLECTION_NUMBER_POSITION]) } .map { Integer.valueOf(it.split(" ")[DefaultCollectionCreationController.DEFAULT_COLLECTION_NUMBER_POSITION]) }
.max() ?: 0) + DefaultCollectionCreationController.DEFAULT_INCREMENT_VALUE .maxOrNull() ?: 0) + DefaultCollectionCreationController.DEFAULT_INCREMENT_VALUE
} }

@ -137,6 +137,9 @@ class HomeFragment : Fragment() {
} }
override fun onCollectionRenamed(tabCollection: TabCollection, title: String) { override fun onCollectionRenamed(tabCollection: TabCollection, title: String) {
lifecycleScope.launch(Main) {
view?.sessionControlRecyclerView?.adapter?.notifyDataSetChanged()
}
showRenamedSnackbar() showRenamedSnackbar()
} }
} }

@ -43,6 +43,9 @@ class TabInCollectionViewHolder(
) )
} }
// This needs to match the elevation of the CollectionViewHolder for the shadow
view.elevation = view.resources.getDimension(R.dimen.home_collection_elevation)
view.setOnClickListener { view.setOnClickListener {
interactor.onCollectionOpenTabClicked(tab) interactor.onCollectionOpenTabClicked(tab)
} }

@ -6,8 +6,8 @@ package org.mozilla.fenix.library.recentlyclosed
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
@ -91,7 +91,7 @@ class RecentlyClosedFragmentView(
overflowView.isVisible = false overflowView.isVisible = false
iconView.background = null iconView.background = null
iconView.setImageDrawable( iconView.setImageDrawable(
ContextCompat.getDrawable( AppCompatResources.getDrawable(
containerView.context, containerView.context,
R.drawable.ic_history R.drawable.ic_history
) )

@ -21,11 +21,11 @@ import android.view.ViewStub
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintProperties.BOTTOM import androidx.constraintlayout.widget.ConstraintProperties.BOTTOM
import androidx.constraintlayout.widget.ConstraintProperties.PARENT_ID import androidx.constraintlayout.widget.ConstraintProperties.PARENT_ID
import androidx.constraintlayout.widget.ConstraintProperties.TOP import androidx.constraintlayout.widget.ConstraintProperties.TOP
import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
@ -431,7 +431,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
private fun addSearchButton(toolbarView: ToolbarView) { private fun addSearchButton(toolbarView: ToolbarView) {
toolbarView.view.addEditAction( toolbarView.view.addEditAction(
BrowserToolbar.Button( BrowserToolbar.Button(
ContextCompat.getDrawable(requireContext(), R.drawable.ic_microphone)!!, AppCompatResources.getDrawable(requireContext(), R.drawable.ic_microphone)!!,
requireContext().getString(R.string.voice_search_content_description), requireContext().getString(R.string.voice_search_content_description),
visible = { visible = {
store.state.searchEngineSource.searchEngine.identifier.contains("google") && store.state.searchEngineSource.searchEngine.identifier.contains("google") &&
@ -481,8 +481,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
private fun updateClipboardSuggestion(searchState: SearchFragmentState, clipboardUrl: String?) { private fun updateClipboardSuggestion(searchState: SearchFragmentState, clipboardUrl: String?) {
val shouldShowView = searchState.showClipboardSuggestions && val shouldShowView = searchState.showClipboardSuggestions &&
searchState.query.isEmpty() && searchState.query.isEmpty() &&
!clipboardUrl.isNullOrEmpty() && !clipboardUrl.isNullOrEmpty()
!searchState.showSearchShortcuts
fill_link_from_clipboard.visibility = if (shouldShowView) View.VISIBLE else View.GONE fill_link_from_clipboard.visibility = if (shouldShowView) View.VISIBLE else View.GONE
clipboard_url.text = clipboardUrl clipboard_url.text = clipboardUrl

@ -9,6 +9,7 @@ import androidx.core.graphics.BlendModeColorFilterCompat.createBlendModeColorFil
import androidx.core.graphics.BlendModeCompat.SRC_IN import androidx.core.graphics.BlendModeCompat.SRC_IN
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import mozilla.components.browser.awesomebar.BrowserAwesomeBar import mozilla.components.browser.awesomebar.BrowserAwesomeBar
import mozilla.components.browser.search.DefaultSearchEngineProvider
import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.concept.awesomebar.AwesomeBar import mozilla.components.concept.awesomebar.AwesomeBar
@ -19,6 +20,7 @@ import mozilla.components.feature.awesomebar.provider.SearchActionProvider
import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.search.SearchUseCases
import mozilla.components.browser.search.ext.toDefaultSearchEngineProvider
import mozilla.components.feature.syncedtabs.DeviceIndicators import mozilla.components.feature.syncedtabs.DeviceIndicators
import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.syncedtabs.SyncedTabsStorageSuggestionProvider import mozilla.components.feature.syncedtabs.SyncedTabsStorageSuggestionProvider
@ -146,7 +148,9 @@ class AwesomeBarView(
defaultSearchSuggestionProvider = defaultSearchSuggestionProvider =
SearchSuggestionProvider( SearchSuggestionProvider(
context = activity, context = activity,
searchEngineManager = components.search.searchEngineManager, defaultSearchEngineProvider = components.search.searchEngineManager.toDefaultSearchEngineProvider(
activity
),
searchUseCase = searchUseCase, searchUseCase = searchUseCase,
fetchClient = components.core.client, fetchClient = components.core.client,
mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS, mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
@ -159,9 +163,9 @@ class AwesomeBarView(
defaultSearchActionProvider = defaultSearchActionProvider =
SearchActionProvider( SearchActionProvider(
searchEngineGetter = suspend { defaultSearchEngineProvider = components.search.searchEngineManager.toDefaultSearchEngineProvider(
components.search.searchEngineManager.getDefaultSearchEngineAsync(activity) activity
}, ),
searchUseCase = searchUseCase, searchUseCase = searchUseCase,
icon = searchBitmap, icon = searchBitmap,
showDescription = false showDescription = false
@ -313,7 +317,11 @@ class AwesomeBarView(
listOf( listOf(
SearchActionProvider( SearchActionProvider(
searchEngineGetter = suspend { searchEngine }, defaultSearchEngineProvider = object : DefaultSearchEngineProvider {
override fun getDefaultSearchEngine(): SearchEngine? = searchEngine
override suspend fun retrieveDefaultSearchEngine(): SearchEngine? =
searchEngine
},
searchUseCase = shortcutSearchUseCase, searchUseCase = shortcutSearchUseCase,
icon = searchBitmap icon = searchBitmap
), ),

@ -45,19 +45,38 @@ class AccessibilityFragment : PreferenceFragmentCompat() {
val components = preference.context.components val components = preference.context.components
// Value is mapped from 0->30 in steps of 1 so let's convert to float in range 0.5->2.0 // Value is mapped from 0->30 in steps of 1 so let's convert to float in range 0.5->2.0
val newTextScale = ((newTextSize * STEP_SIZE) + MIN_SCALE_VALUE).toFloat() / PERCENT_TO_DECIMAL val newTextScale =
((newTextSize * STEP_SIZE) + MIN_SCALE_VALUE).toFloat() / PERCENT_TO_DECIMAL
// If scale is 100%, use the automatic font size adjustment // Save new text scale value. We assume auto sizing is off if this change listener was called.
val useAutoSize = newTextScale == 1F settings.fontSizeFactor = newTextScale
components.core.engine.settings.fontSizeFactor = newTextScale
// Reload the current session to reflect the new text scale
components.useCases.sessionUseCases.reload()
true
}
textSizePreference.isEnabled = !requireContext().settings().shouldUseAutoSize
val useAutoSizePreference =
requirePreference<SwitchPreference>(R.string.pref_key_accessibility_auto_size)
useAutoSizePreference.setOnPreferenceChangeListener<Boolean> { preference, useAutoSize ->
val settings = preference.context.settings()
val components = preference.context.components
// Save the new setting value
settings.shouldUseAutoSize = useAutoSize
components.core.engine.settings.automaticFontSizeAdjustment = useAutoSize components.core.engine.settings.automaticFontSizeAdjustment = useAutoSize
components.core.engine.settings.fontInflationEnabled = useAutoSize components.core.engine.settings.fontInflationEnabled = useAutoSize
// If using manual sizing, update the engine settings with the new scale // If using manual sizing, update the engine settings with the local saved setting
if (!useAutoSize) { if (!useAutoSize) {
settings.fontSizeFactor = newTextScale components.core.engine.settings.fontSizeFactor = settings.fontSizeFactor
components.core.engine.settings.fontSizeFactor = newTextScale
} }
// Enable the manual sizing controls if automatic sizing is turned off.
textSizePreference.isEnabled = !useAutoSize
// Reload the current session to reflect the new text scale // Reload the current session to reflect the new text scale
components.useCases.sessionUseCases.reload() components.useCases.sessionUseCases.reload()
true true

@ -12,7 +12,6 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.FeatureFlags
@ -219,12 +218,7 @@ class CustomizationFragment : PreferenceFragmentCompat() {
} }
private fun setupHomeCategory() { private fun setupHomeCategory() {
requirePreference<PreferenceCategory>(R.string.pref_home_category).apply {
isVisible = FeatureFlags.topFrecentSite
}
requirePreference<SwitchPreference>(R.string.pref_key_enable_top_frecent_sites).apply { requirePreference<SwitchPreference>(R.string.pref_key_enable_top_frecent_sites).apply {
isVisible = FeatureFlags.topFrecentSite
isChecked = context.settings().showTopFrecentSites isChecked = context.settings().showTopFrecentSites
onPreferenceChangeListener = SharedPreferenceUpdater() onPreferenceChangeListener = SharedPreferenceUpdater()
} }

@ -24,8 +24,7 @@ class DataChoicesFragment : PreferenceFragmentCompat() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val context = requireContext() val context = requireContext()
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) { preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) { _, key ->
_, key ->
if (key == getPreferenceKey(R.string.pref_key_telemetry)) { if (key == getPreferenceKey(R.string.pref_key_telemetry)) {
if (context.settings().isTelemetryEnabled) { if (context.settings().isTelemetryEnabled) {
context.components.analytics.metrics.start(MetricServiceType.Data) context.components.analytics.metrics.start(MetricServiceType.Data)
@ -63,7 +62,10 @@ class DataChoicesFragment : PreferenceFragmentCompat() {
isChecked = context.settings().isMarketingTelemetryEnabled isChecked = context.settings().isMarketingTelemetryEnabled
val appName = context.getString(R.string.app_name) val appName = context.getString(R.string.app_name)
summary = context.getString(R.string.preferences_marketing_data_description, appName) summary = String.format(
context.getString(R.string.preferences_marketing_data_description),
appName
)
onPreferenceChangeListener = SharedPreferenceUpdater() onPreferenceChangeListener = SharedPreferenceUpdater()
} }

@ -7,8 +7,6 @@ package org.mozilla.fenix.settings
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.ImageView import android.widget.ImageView
import androidx.core.content.res.TypedArrayUtils
import androidx.core.content.withStyledAttributes
import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -32,29 +30,6 @@ class RadioButtonInfoPreference @JvmOverloads constructor(
init { init {
layoutResource = R.layout.preference_widget_radiobutton_with_info layoutResource = R.layout.preference_widget_radiobutton_with_info
context.withStyledAttributes(
attrs,
androidx.preference.R.styleable.Preference,
TypedArrayUtils.getAttr(
context,
androidx.preference.R.attr.preferenceStyle,
android.R.attr.preferenceStyle
),
0
) {
val defaultValue = when {
hasValue(androidx.preference.R.styleable.Preference_defaultValue) ->
getBoolean(androidx.preference.R.styleable.Preference_defaultValue, false)
hasValue(androidx.preference.R.styleable.Preference_android_defaultValue) ->
getBoolean(
androidx.preference.R.styleable.Preference_android_defaultValue,
false
)
else -> false
}
setDefaultValue(defaultValue)
}
} }
override fun onBindViewHolder(holder: PreferenceViewHolder) { override fun onBindViewHolder(holder: PreferenceViewHolder) {

@ -31,12 +31,6 @@ class SecretSettingsFragment : PreferenceFragmentCompat() {
onPreferenceChangeListener = SharedPreferenceUpdater() onPreferenceChangeListener = SharedPreferenceUpdater()
} }
requirePreference<SwitchPreference>(R.string.pref_key_enable_top_frecent_sites).apply {
isVisible = FeatureFlags.topFrecentSite
isChecked = context.settings().showTopFrecentSites
onPreferenceChangeListener = SharedPreferenceUpdater()
}
requirePreference<SwitchPreference>(R.string.pref_key_wait_first_paint).apply { requirePreference<SwitchPreference>(R.string.pref_key_wait_first_paint).apply {
isVisible = FeatureFlags.waitUntilPaintToDraw isVisible = FeatureFlags.waitUntilPaintToDraw
isChecked = context.settings().waitToShowPageUntilFirstPaint isChecked = context.settings().waitToShowPageUntilFirstPaint

@ -378,6 +378,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
} }
preferenceExternalDownloadManager.isVisible = FeatureFlags.externalDownloadManager preferenceExternalDownloadManager.isVisible = FeatureFlags.externalDownloadManager
preferenceRemoteDebugging?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
preferenceRemoteDebugging?.setOnPreferenceChangeListener<Boolean> { preference, newValue -> preferenceRemoteDebugging?.setOnPreferenceChangeListener<Boolean> { preference, newValue ->
preference.context.settings().preferences.edit() preference.context.settings().preferences.edit()
.putBoolean(preference.key, newValue).apply() .putBoolean(preference.key, newValue).apply()

@ -39,7 +39,6 @@ object SupportUtils {
SET_AS_DEFAULT_BROWSER("set-firefox-preview-default"), SET_AS_DEFAULT_BROWSER("set-firefox-preview-default"),
SEARCH_SUGGESTION("how-search-firefox-preview"), SEARCH_SUGGESTION("how-search-firefox-preview"),
CUSTOM_SEARCH_ENGINES("custom-search-engines"), CUSTOM_SEARCH_ENGINES("custom-search-engines"),
UPGRADE_FAQ("firefox-preview-upgrade-faqs"),
SYNC_SETUP("how-set-firefox-sync-firefox-preview"), SYNC_SETUP("how-set-firefox-sync-firefox-preview"),
QR_CAMERA_ACCESS("qr-camera-access") QR_CAMERA_ACCESS("qr-camera-access")
} }
@ -85,9 +84,7 @@ object SupportUtils {
return "https://www.mozilla.org/$langTag/$path" return "https://www.mozilla.org/$langTag/$path"
} }
fun getWhatsNewUrl(context: Context) = if (Config.channel.isFennec) { fun getWhatsNewUrl(context: Context) = if (Config.channel.isFork) {
getGenericSumoURLForTopic(SumoTopic.UPGRADE_FAQ)
} else if (Config.channel.isFork) {
"https://github.com/fork-maintainers/iceraven-browser/releases" "https://github.com/fork-maintainers/iceraven-browser/releases"
} else { } else {
getSumoURLForTopic(context, SumoTopic.WHATS_NEW) getSumoURLForTopic(context, SumoTopic.WHATS_NEW)

@ -5,8 +5,12 @@
package org.mozilla.fenix.settings package org.mozilla.fenix.settings
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.utils.view.addToRadioGroup import org.mozilla.fenix.utils.view.addToRadioGroup
@ -23,6 +27,11 @@ class TabsSettingsFragment : PreferenceFragmentCompat() {
setPreferencesFromResource(R.xml.tabs_preferences, rootKey) setPreferencesFromResource(R.xml.tabs_preferences, rootKey)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.context.components.analytics.metrics.track(Event.TabSettingsOpened)
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
showToolbar(getString(R.string.preferences_tabs)) showToolbar(getString(R.string.preferences_tabs))

@ -67,30 +67,37 @@ class TextPercentageSeekBarPreference @JvmOverloads constructor(
) : Preference(context, attrs, defStyleAttr, defStyleRes) { ) : Preference(context, attrs, defStyleAttr, defStyleRes) {
/* synthetic access */ /* synthetic access */
internal var mSeekBarValue: Int = 0 internal var mSeekBarValue: Int = 0
/* synthetic access */ /* synthetic access */
internal var mMin: Int = 0 internal var mMin: Int = 0
private var mMax: Int = 0 private var mMax: Int = 0
private var mSeekBarIncrement: Int = 0 private var mSeekBarIncrement: Int = 0
/* synthetic access */ /* synthetic access */
internal var mTrackingTouch: Boolean = false internal var mTrackingTouch: Boolean = false
/* synthetic access */ /* synthetic access */
internal var mSeekBar: SeekBar? = null internal var mSeekBar: SeekBar? = null
private var mSeekBarValueTextView: TextView? = null private var mSeekBarValueTextView: TextView? = null
private var mExampleTextTextView: TextView? = null private var mExampleTextTextView: TextView? = null
/** /**
* Whether the SeekBar should respond to the left/right keys * Whether the SeekBar should respond to the left/right keys
*/ */
/* synthetic access */ /* synthetic access */
var isAdjustable: Boolean = false var isAdjustable: Boolean = false
/** /**
* Whether to show the SeekBar value TextView next to the bar * Whether to show the SeekBar value TextView next to the bar
*/ */
private var mShowSeekBarValue: Boolean = false private var mShowSeekBarValue: Boolean = false
/** /**
* Whether the SeekBarPreference should continuously save the Seekbar value while it is being dragged. * Whether the SeekBarPreference should continuously save the Seekbar value while it is being dragged.
*/ */
/* synthetic access */ /* synthetic access */
var updatesContinuously: Boolean = false var updatesContinuously: Boolean = false
/** /**
* Listener reacting to the [SeekBar] changing value by the user * Listener reacting to the [SeekBar] changing value by the user
*/ */
@ -273,6 +280,8 @@ class TextPercentageSeekBarPreference @JvmOverloads constructor(
updateExampleTextValue(mSeekBarValue) updateExampleTextValue(mSeekBarValue)
updateLabelValue(mSeekBarValue) updateLabelValue(mSeekBarValue)
mSeekBar?.isEnabled = isEnabled mSeekBar?.isEnabled = isEnabled
mSeekBarValueTextView?.alpha = if (isEnabled) 1F else HALF_ALPHA
mExampleTextTextView?.alpha = if (isEnabled) 1F else HALF_ALPHA
mSeekBar?.let { mSeekBar?.let {
it.thumbOffset = it.thumb.intrinsicWidth.div(2 * PI).roundToInt() it.thumbOffset = it.thumb.intrinsicWidth.div(2 * PI).roundToInt()
} }
@ -461,6 +470,7 @@ class TextPercentageSeekBarPreference @JvmOverloads constructor(
companion object { companion object {
private const val TAG = "SeekBarPreference" private const val TAG = "SeekBarPreference"
private const val STEP_SIZE = 5 private const val STEP_SIZE = 5
private const val HALF_ALPHA = 0.5F
private const val MIN_VALUE = 50 private const val MIN_VALUE = 50
private const val DECIMAL_CONVERSION = 100f private const val DECIMAL_CONVERSION = 100f
private const val TEXT_SIZE = 16f private const val TEXT_SIZE = 16f

@ -403,8 +403,8 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
) )
} }
} }
is LastSyncTime.Success -> getString( is LastSyncTime.Success -> String.format(
R.string.sync_last_synced_summary, getString(R.string.sync_last_synced_summary),
DateUtils.getRelativeTimeSpanString(state.lastSyncedDate.lastSync) DateUtils.getRelativeTimeSpanString(state.lastSyncedDate.lastSync)
) )
} }

@ -50,8 +50,10 @@ class SignOutFragment : BottomSheetDialogFragment() {
): View? { ): View? {
accountManager = requireComponents.backgroundServices.accountManager accountManager = requireComponents.backgroundServices.accountManager
val view = inflater.inflate(R.layout.fragment_sign_out, container, false) val view = inflater.inflate(R.layout.fragment_sign_out, container, false)
view.sign_out_message.text = view.context.getString( view.sign_out_message.text = String.format(
R.string.sign_out_confirmation_message_2, view.context.getString(
R.string.sign_out_confirmation_message_2
),
view.context.getString(R.string.app_name) view.context.getString(R.string.app_name)
) )
return view return view

@ -20,8 +20,9 @@ class LocaleViewHolder(
) : BaseLocaleViewHolder(view, selectedLocale) { ) : BaseLocaleViewHolder(view, selectedLocale) {
override fun bind(locale: Locale) { override fun bind(locale: Locale) {
// capitalisation is done using the rules of the appropriate locale (endonym and exonym) // Capitalisation is done using the rules of the appropriate locale (endonym and exonym).
locale_title_text.text = locale.getDisplayName(locale).capitalize(locale) locale_title_text.text = locale.getDisplayName(locale).capitalize(locale)
// Show the given locale using the device locale for the subtitle.
locale_subtitle_text.text = locale.displayName.capitalize(Locale.getDefault()) locale_subtitle_text.text = locale.displayName.capitalize(Locale.getDefault())
locale_selected_icon.isVisible = isCurrentLocaleSelected(locale, isDefault = false) locale_selected_icon.isVisible = isCurrentLocaleSelected(locale, isDefault = false)
@ -39,7 +40,8 @@ class SystemLocaleViewHolder(
override fun bind(locale: Locale) { override fun bind(locale: Locale) {
locale_title_text.text = itemView.context.getString(R.string.default_locale_text) locale_title_text.text = itemView.context.getString(R.string.default_locale_text)
locale_subtitle_text.visibility = View.GONE // Use the device locale for the system locale subtitle.
locale_subtitle_text.text = locale.getDisplayName(locale).capitalize(locale)
locale_selected_icon.isVisible = isCurrentLocaleSelected(locale, isDefault = true) locale_selected_icon.isVisible = isCurrentLocaleSelected(locale, isDefault = true)
itemView.setOnClickListener { itemView.setOnClickListener {
interactor.onDefaultLocaleSelected() interactor.onDefaultLocaleSelected()

@ -141,36 +141,7 @@ class AddSearchEngineFragment : Fragment(R.layout.fragment_add_search_engine),
val name = edit_engine_name.text?.toString()?.trim() ?: "" val name = edit_engine_name.text?.toString()?.trim() ?: ""
val searchString = edit_search_string.text?.toString() ?: "" val searchString = edit_search_string.text?.toString() ?: ""
var hasError = false val hasError = checkForErrors(name, searchString)
if (name.isEmpty()) {
custom_search_engine_name_field.error = resources
.getString(R.string.search_add_custom_engine_error_empty_name)
hasError = true
}
val existingIdentifiers = requireComponents
.search
.provider
.allSearchEngineIdentifiers()
.map { it.toLowerCase(Locale.ROOT) }
if (existingIdentifiers.contains(name.toLowerCase(Locale.ROOT))) {
custom_search_engine_name_field.error = resources
.getString(R.string.search_add_custom_engine_error_existing_name, name)
hasError = true
}
custom_search_engine_search_string_field.error = when {
searchString.isEmpty() ->
resources.getString(R.string.search_add_custom_engine_error_empty_search_string)
!searchString.contains("%s") ->
resources.getString(R.string.search_add_custom_engine_error_missing_template)
else -> null
}
if (custom_search_engine_search_string_field.error != null) {
hasError = true
}
if (hasError) { return } if (hasError) { return }
@ -188,17 +159,28 @@ class AddSearchEngineFragment : Fragment(R.layout.fragment_add_search_engine),
.getString(R.string.search_add_custom_engine_error_cannot_reach, name) .getString(R.string.search_add_custom_engine_error_cannot_reach, name)
} }
SearchStringValidator.Result.Success -> { SearchStringValidator.Result.Success -> {
CustomSearchEngineStore.addSearchEngine( try {
context = requireContext(), CustomSearchEngineStore.addSearchEngine(
engineName = name, context = requireContext(),
searchQuery = searchString engineName = name,
) searchQuery = searchString
)
} catch (engineNameExists: CustomSearchEngineStore.EngineNameAlreadyExists) {
custom_search_engine_name_field.error =
String.format(
resources.getString(
R.string.search_add_custom_engine_error_existing_name
), name
)
return@launch
}
requireComponents.search.provider.reload() requireComponents.search.provider.reload()
val successMessage = resources val successMessage = resources
.getString(R.string.search_add_custom_engine_success_message, name) .getString(R.string.search_add_custom_engine_success_message, name)
view?.also { view?.also {
FenixSnackbar.make(view = it, FenixSnackbar.make(
view = it,
duration = FenixSnackbar.LENGTH_SHORT, duration = FenixSnackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = false isDisplayedWithBrowserToolbar = false
) )
@ -213,6 +195,44 @@ class AddSearchEngineFragment : Fragment(R.layout.fragment_add_search_engine),
} }
} }
fun checkForErrors(name: String, searchString: String): Boolean {
val existingIdentifiers = requireComponents
.search
.provider
.allSearchEngineIdentifiers()
.map { it.toLowerCase(Locale.ROOT) }
val hasError = when {
name.isEmpty() -> {
custom_search_engine_name_field.error = resources
.getString(R.string.search_add_custom_engine_error_empty_name)
true
}
existingIdentifiers.contains(name.toLowerCase(Locale.ROOT)) -> {
custom_search_engine_name_field.error =
String.format(
resources.getString(
R.string.search_add_custom_engine_error_existing_name
), name
)
true
}
searchString.isEmpty() -> {
custom_search_engine_search_string_field.error =
resources.getString(R.string.search_add_custom_engine_error_empty_search_string)
true
}
!searchString.contains("%s") -> {
custom_search_engine_search_string_field.error =
resources.getString(R.string.search_add_custom_engine_error_missing_template)
true
}
else -> false
}
return hasError
}
private fun installEngine(engine: SearchEngine) { private fun installEngine(engine: SearchEngine) {
viewLifecycleOwner.lifecycleScope.launch(Main) { viewLifecycleOwner.lifecycleScope.launch(Main) {
withContext(IO) { withContext(IO) {
@ -281,6 +301,5 @@ class AddSearchEngineFragment : Fragment(R.layout.fragment_add_search_engine),
private const val DISABLED_ALPHA = 0.2f private const val DISABLED_ALPHA = 0.2f
private const val CUSTOM_INDEX = -1 private const val CUSTOM_INDEX = -1
private const val FIRST_INDEX = 0 private const val FIRST_INDEX = 0
private const val DPS_TO_INCREASE = 20
} }
} }

@ -36,7 +36,6 @@ import java.util.Locale
class EditCustomSearchEngineFragment : Fragment(R.layout.fragment_add_search_engine) { class EditCustomSearchEngineFragment : Fragment(R.layout.fragment_add_search_engine) {
private val args by navArgs<EditCustomSearchEngineFragmentArgs>() private val args by navArgs<EditCustomSearchEngineFragmentArgs>()
private lateinit var searchEngine: SearchEngine private lateinit var searchEngine: SearchEngine
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -85,6 +84,7 @@ class EditCustomSearchEngineFragment : Fragment(R.layout.fragment_add_search_eng
} }
} }
@Suppress("LongMethod")
private fun saveCustomEngine() { private fun saveCustomEngine() {
custom_search_engine_name_field.error = "" custom_search_engine_name_field.error = ""
custom_search_engine_search_string_field.error = "" custom_search_engine_search_string_field.error = ""
@ -92,42 +92,13 @@ class EditCustomSearchEngineFragment : Fragment(R.layout.fragment_add_search_eng
val name = edit_engine_name.text?.toString()?.trim() ?: "" val name = edit_engine_name.text?.toString()?.trim() ?: ""
val searchString = edit_search_string.text?.toString() ?: "" val searchString = edit_search_string.text?.toString() ?: ""
var hasError = false val hasError = checkForErrors(name, searchString)
if (name.isEmpty()) {
custom_search_engine_name_field.error = resources
.getString(R.string.search_add_custom_engine_error_empty_name)
hasError = true
}
val existingIdentifiers = requireComponents if (hasError) {
.search return
.provider
.allSearchEngineIdentifiers()
.map { it.toLowerCase(Locale.ROOT) }
val nameHasChanged = name != args.searchEngineIdentifier
if (existingIdentifiers.contains(name.toLowerCase(Locale.ROOT)) && nameHasChanged) {
custom_search_engine_name_field.error = resources
.getString(R.string.search_add_custom_engine_error_existing_name, name)
hasError = true
}
if (searchString.isEmpty()) {
custom_search_engine_search_string_field
.error = resources.getString(R.string.search_add_custom_engine_error_empty_search_string)
hasError = true
}
if (!searchString.contains("%s")) {
custom_search_engine_search_string_field
.error = resources.getString(R.string.search_add_custom_engine_error_missing_template)
hasError = true
} }
if (hasError) { return } lifecycleScope.launch(Main) {
viewLifecycleOwner.lifecycleScope.launch(Main) {
val result = withContext(IO) { val result = withContext(IO) {
SearchStringValidator.isSearchStringValid( SearchStringValidator.isSearchStringValid(
requireComponents.core.client, requireComponents.core.client,
@ -160,14 +131,50 @@ class EditCustomSearchEngineFragment : Fragment(R.layout.fragment_add_search_eng
.setText(successMessage) .setText(successMessage)
.show() .show()
} }
if (args.isDefaultSearchEngine) {
requireComponents.search.provider.setDefaultEngine(requireContext(), name)
}
findNavController().popBackStack() findNavController().popBackStack()
} }
} }
} }
} }
companion object { private fun checkForErrors(name: String, searchString: String): Boolean {
private const val DPS_TO_INCREASE = 20 val existingIdentifiers = requireComponents
.search
.provider
.allSearchEngineIdentifiers()
.map { it.toLowerCase(Locale.ROOT) }
val nameHasChanged = name != args.searchEngineIdentifier
val hasError = when {
name.isEmpty() -> {
custom_search_engine_name_field.error = resources
.getString(R.string.search_add_custom_engine_error_empty_name)
true
}
existingIdentifiers.contains(name.toLowerCase(Locale.ROOT)) && nameHasChanged -> {
custom_search_engine_name_field.error =
String.format(
resources.getString(
R.string.search_add_custom_engine_error_existing_name
), name
)
true
}
searchString.isEmpty() -> {
custom_search_engine_search_string_field.error =
resources.getString(R.string.search_add_custom_engine_error_empty_search_string)
true
}
!searchString.contains("%s") -> {
custom_search_engine_search_string_field.error =
resources.getString(R.string.search_add_custom_engine_error_missing_template)
true
}
else -> false
}
return hasError
} }
} }

@ -25,7 +25,7 @@ class RadioSearchEngineListPreference @JvmOverloads constructor(
} }
override fun onSearchEngineSelected(searchEngine: SearchEngine) { override fun onSearchEngineSelected(searchEngine: SearchEngine) {
context.components.search.searchEngineManager.defaultSearchEngine = searchEngine context.components.search.provider.setDefaultEngine(context, searchEngine.identifier)
context.settings().defaultSearchEngineName = searchEngine.name context.settings().defaultSearchEngineName = searchEngine.name
} }
} }

@ -10,6 +10,7 @@ import androidx.preference.CheckBoxPreference
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 mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -21,10 +22,12 @@ class SearchEngineFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.search_preferences, rootKey) setPreferencesFromResource(R.xml.search_preferences, rootKey)
view?.hideKeyboard()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
view?.hideKeyboard()
showToolbar(getString(R.string.preferences_search)) showToolbar(getString(R.string.preferences_search))
val searchSuggestionsPreference = val searchSuggestionsPreference =

@ -71,15 +71,14 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
return return
} }
val defaultEngine = context.components.search.provider.getDefaultEngine(context).identifier val defaultEngineId = context.components.search.provider.getDefaultEngine(context).identifier
val selectedEngine = (searchEngineList.list.find { val selectedEngine = (searchEngineList.list.find {
it.identifier == defaultEngine it.identifier == defaultEngineId
} ?: searchEngineList.list.first()).identifier } ?: searchEngineList.list.first()).identifier
context.components.search.searchEngineManager.defaultSearchEngine = // set the search engine manager default
searchEngineList.list.find { context.components.search.provider.setDefaultEngine(context, selectedEngine)
it.identifier == selectedEngine
}
searchEngineGroup!!.removeAllViews() searchEngineGroup!!.removeAllViews()
@ -175,8 +174,9 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
} }
private fun editCustomSearchEngine(engine: SearchEngine) { private fun editCustomSearchEngine(engine: SearchEngine) {
val wasDefault = context.components.search.provider.getDefaultEngine(context).identifier == engine.identifier
val directions = SearchEngineFragmentDirections val directions = SearchEngineFragmentDirections
.actionSearchEngineFragmentToEditCustomSearchEngineFragment(engine.identifier) .actionSearchEngineFragmentToEditCustomSearchEngineFragment(engine.identifier, wasDefault)
Navigation.findNavController(searchEngineGroup!!).navigate(directions) Navigation.findNavController(searchEngineGroup!!).navigate(directions)
} }
@ -215,12 +215,9 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
}, },
operation = { operation = {
if (isDefaultEngine) { if (isDefaultEngine) {
context.settings().defaultSearchEngineName = context val default = context.components.search.provider.getDefaultEngine(context)
.components context.components.search.provider.setDefaultEngine(context, default.identifier)
.search context.settings().defaultSearchEngineName = default.name
.provider
.getDefaultEngine(context)
.name
} }
if (isCustomSearchEngine) { if (isCustomSearchEngine) {
context.components.analytics.metrics.track(Event.CustomEngineDeleted) context.components.analytics.metrics.track(Event.CustomEngineDeleted)

@ -26,6 +26,8 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.share.listadapters.AppShareOption
import org.mozilla.fenix.share.listadapters.SyncShareOption
class ShareFragment : AppCompatDialogFragment() { class ShareFragment : AppCompatDialogFragment() {
@ -111,13 +113,13 @@ class ShareFragment : AppCompatDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.devicesList.observe(viewLifecycleOwner) { devicesShareOptions -> viewModel.devicesList.observe<List<SyncShareOption>>(viewLifecycleOwner) { devicesShareOptions ->
shareToAccountDevicesView.setShareTargets(devicesShareOptions) shareToAccountDevicesView.setShareTargets(devicesShareOptions)
} }
viewModel.appsList.observe(viewLifecycleOwner) { appsToShareTo -> viewModel.appsList.observe<List<AppShareOption>>(viewLifecycleOwner) { appsToShareTo ->
shareToAppsView.setShareTargets(appsToShareTo) shareToAppsView.setShareTargets(appsToShareTo)
} }
viewModel.recentAppsList.observe(viewLifecycleOwner) { appsToShareTo -> viewModel.recentAppsList.observe<List<AppShareOption>>(viewLifecycleOwner) { appsToShareTo ->
shareToAppsView.setRecentShareTargets(appsToShareTo) shareToAppsView.setRecentShareTargets(appsToShareTo)
} }
} }

@ -393,6 +393,8 @@ class TabTrayView(
} else { } else {
setupRegularTabsTrayLayout() setupRegularTabsTrayLayout()
} }
// We will need to learn call setupGridTabView(), Mozilla's new
// official grid layout, by preference.
} }
private fun setupCompactTabsTrayLayout() { private fun setupCompactTabsTrayLayout() {
@ -465,8 +467,6 @@ class TabTrayView(
updateUINormalMode(view.context.components.core.store.state) updateUINormalMode(view.context.components.core.store.state)
scrollToTab(view.context.components.core.store.state.selectedTabId) scrollToTab(view.context.components.core.store.state.selectedTabId)
view.tabsTray.invalidateItemDecorations()
if (isPrivateModeSelected) { if (isPrivateModeSelected) {
components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped) components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped)
} else { } else {
@ -480,6 +480,26 @@ class TabTrayView(
var mode: Mode = Mode.Normal var mode: Mode = Mode.Normal
private set private set
private fun setupGridTabView() {
view.tabsTray.apply {
val gridLayoutManager =
GridLayoutManager(container.context, getNumberOfGridColumns(container.context))
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
val numTabs = tabsAdapter.itemCount
return if (position < numTabs) {
1
} else {
getNumberOfGridColumns(container.context)
}
}
}
layoutManager = gridLayoutManager
}
}
/** /**
* Returns the number of columns that will fit in the grid layout for the current screen. * Returns the number of columns that will fit in the grid layout for the current screen.
*/ */
@ -688,7 +708,7 @@ class TabTrayView(
view.tab_layout.getTabAt(0)?.contentDescription = if (count == 1) { view.tab_layout.getTabAt(0)?.contentDescription = if (count == 1) {
view.context?.getString(R.string.open_tab_tray_single) view.context?.getString(R.string.open_tab_tray_single)
} else { } else {
view.context?.getString(R.string.open_tab_tray_plural, count.toString()) String.format(view.context.getString(R.string.open_tab_tray_plural), count.toString())
} }
view.tabsTray.accessibilityDelegate = object : View.AccessibilityDelegate() { view.tabsTray.accessibilityDelegate = object : View.AccessibilityDelegate() {
@ -774,6 +794,10 @@ class TabTrayView(
} }
layoutManager?.scrollToPosition(recyclerViewIndex) layoutManager?.scrollToPosition(recyclerViewIndex)
smoothScrollBy(
0,
- resources.getDimensionPixelSize(R.dimen.tab_tray_tab_item_height) / 2
)
} }
} }

@ -100,10 +100,9 @@ class Settings(private val appContext: Context) : PreferencesHolder {
override val preferences: SharedPreferences = override val preferences: SharedPreferences =
appContext.getSharedPreferences(FENIX_PREFERENCES, MODE_PRIVATE) appContext.getSharedPreferences(FENIX_PREFERENCES, MODE_PRIVATE)
var showTopFrecentSites by featureFlagPreference( var showTopFrecentSites by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_enable_top_frecent_sites), appContext.getPreferenceKey(R.string.pref_key_enable_top_frecent_sites),
default = true, default = true
featureFlag = FeatureFlags.topFrecentSite
) )
var numberOfAppLaunches by intPreference( var numberOfAppLaunches by intPreference(
@ -182,6 +181,11 @@ class Settings(private val appContext: Context) : PreferencesHolder {
true true
) )
var shouldReturnToBrowser by booleanPreference(
appContext.getString(R.string.pref_key_return_to_browser),
false
)
// If any of the prefs have been modified, quit displaying the fenix moved tip // If any of the prefs have been modified, quit displaying the fenix moved tip
fun shouldDisplayFenixMovingTip(): Boolean = fun shouldDisplayFenixMovingTip(): Boolean =
preferences.getBoolean( preferences.getBoolean(
@ -311,13 +315,16 @@ class Settings(private val appContext: Context) : PreferencesHolder {
val shouldShowSecurityPinWarning: Boolean val shouldShowSecurityPinWarning: Boolean
get() = loginsSecureWarningCount.underMaxCount() get() = loginsSecureWarningCount.underMaxCount()
fun shouldUseAutoSize() = fontSizeFactor == 1F
var shouldUseLightTheme by booleanPreference( var shouldUseLightTheme by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_light_theme), appContext.getPreferenceKey(R.string.pref_key_light_theme),
default = false default = false
) )
var shouldUseAutoSize by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_accessibility_auto_size),
default = true
)
var fontSizeFactor by floatPreference( var fontSizeFactor by floatPreference(
appContext.getPreferenceKey(R.string.pref_key_accessibility_font_scale), appContext.getPreferenceKey(R.string.pref_key_accessibility_font_scale),
default = 1f default = 1f
@ -348,6 +355,16 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = false default = false
) )
var listTabView by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_tab_view_list),
default = true
)
var gridTabView by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_tab_view_grid),
default = false
)
var manuallyCloseTabs by booleanPreference( var manuallyCloseTabs by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_close_tabs_manually), appContext.getPreferenceKey(R.string.pref_key_close_tabs_manually),
default = true default = true
@ -375,6 +392,31 @@ class Settings(private val appContext: Context) : PreferencesHolder {
else -> System.currentTimeMillis() else -> System.currentTimeMillis()
} }
enum class TabView {
GRID, LIST
}
fun getTabViewPingString() = if (gridTabView) TabView.GRID.name else TabView.LIST.name
enum class TabTimout {
ONE_DAY, ONE_WEEK, ONE_MONTH, MANUAL
}
fun getTabTimeoutPingString(): String = when {
closeTabsAfterOneDay -> {
TabTimout.ONE_DAY.name
}
closeTabsAfterOneWeek -> {
TabTimout.ONE_WEEK.name
}
closeTabsAfterOneMonth -> {
TabTimout.ONE_MONTH.name
}
else -> {
TabTimout.MANUAL.name
}
}
fun getTabTimeoutString(): String = when { fun getTabTimeoutString(): String = when {
closeTabsAfterOneDay -> { closeTabsAfterOneDay -> {
appContext.getString(R.string.close_tabs_after_one_day) appContext.getString(R.string.close_tabs_after_one_day)
@ -914,7 +956,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
return overrideAmoUser.isNotEmpty() || overrideAmoCollection.isNotEmpty() return overrideAmoUser.isNotEmpty() || overrideAmoCollection.isNotEmpty()
} }
val topSitesSize by intPreference( var topSitesSize by intPreference(
appContext.getPreferenceKey(R.string.pref_key_top_sites_size), appContext.getPreferenceKey(R.string.pref_key_top_sites_size),
default = 0 default = 0
) )
@ -924,18 +966,10 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = topSitesMaxCount default = topSitesMaxCount
) )
fun setOpenTabsCount(count: Int) { var openTabsCount by intPreference(
preferences.edit().putInt( appContext.getPreferenceKey(R.string.pref_key_open_tabs_count),
appContext.getPreferenceKey(R.string.pref_key_open_tabs_count), 0
count )
).apply()
}
val openTabsCount: Int
get() = preferences.getInt(
appContext.getPreferenceKey(R.string.pref_key_open_tabs_count),
0
)
val customAddonsAccount by stringPreference( val customAddonsAccount by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_addons_custom_account), appContext.getPreferenceKey(R.string.pref_key_addons_custom_account),

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#fbfbfe"
android:pathData="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:strokeWidth="1" />
<path
android:name="path_1"
android:pathData="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:angle="45"
android:endColor="#00b3f4"
android:endX="0.0"
android:endY="20.0"
android:startColor="#C689FF"
android:startX="24.0"
android:startY="10.0"
android:type="linear" />
</aapt:attr>
</path>
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="330"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:valueTo="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:valueType="pathType" />
<objectAnimator
android:duration="330"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_1">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="330"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:valueTo="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:valueType="pathType" />
<objectAnimator
android:duration="330"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
</animated-vector>

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#20123a"
android:pathData="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:strokeWidth="1" />
<path
android:name="path_1"
android:pathData="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:angle="45"
android:endColor="#0250BB"
android:endX="0.0"
android:endY="20.0"
android:startColor="#9059FF"
android:startX="24.0"
android:startY="10.0"
android:type="linear" />
</aapt:attr>
</path>
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="330"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:valueTo="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:valueType="pathType" />
<objectAnimator
android:duration="330"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_1">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="330"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:valueTo="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:valueType="pathType" />
<objectAnimator
android:duration="330"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
</animated-vector>

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#fbfbfe"
android:pathData="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:strokeWidth="1" />
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="fillColor"
android:valueFrom="#fbfbfe"
android:valueTo="#00b3f4"
android:valueType="colorType" />
</aapt:attr>
</target>
</animated-vector>

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#20123a"
android:pathData="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z"
android:strokeWidth="1" />
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="fillColor"
android:valueFrom="#20123a"
android:valueTo="#0250bb"
android:valueType="colorType" />
</aapt:attr>
</target>
</animated-vector>

@ -13,6 +13,7 @@
<ImageView <ImageView
android:id="@android:id/icon" android:id="@android:id/icon"
android:importantForAccessibility="no"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_gravity="center" android:layout_gravity="center"

@ -20,9 +20,11 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<!-- MozMultipleConstraintLayouts: we're not changing the migration code. -->
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
tools:ignore="MozMultipleConstraintLayouts">
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/migration_firefox_logo" android:id="@+id/migration_firefox_logo"

@ -3,30 +3,34 @@
- License, v. 2.0. If a copy of the MPL was not distributed with this - 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/. --> - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linear_layout" android:id="@+id/linear_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<EditText <EditText
android:id="@+id/custom_amo_user" android:id="@+id/custom_amo_user"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/customize_addon_collection_user_hint"
android:singleLine="true"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:textColor="?primaryText"/> android:layout_marginEnd="16dp"
android:autofillHints="false"
android:hint="@string/customize_addon_collection_user_hint"
android:inputType="number"
android:singleLine="true"
android:textColor="?primaryText" />
<EditText <EditText
android:id="@+id/custom_amo_collection" android:id="@+id/custom_amo_collection"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/customize_addon_collection_hint"
android:singleLine="true"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:textColor="?primaryText"/> android:autofillHints="false"
android:hint="@string/customize_addon_collection_hint"
android:inputType="text"
android:singleLine="true"
android:textColor="?primaryText" />
</LinearLayout> </LinearLayout>

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public <?xml version="1.0" encoding="utf-8"?>
<!-- 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 - 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/. --> - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
@ -14,6 +15,7 @@
<Button <Button
android:id="@+id/copy" android:id="@+id/copy"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
@ -26,6 +28,7 @@
<Button <Button
android:id="@+id/paste" android:id="@+id/paste"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
@ -38,6 +41,7 @@
<Button <Button
android:id="@+id/paste_and_go" android:id="@+id/paste_and_go"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"

@ -35,7 +35,7 @@
android:layout_height="48dp" android:layout_height="48dp"
android:background="?selectableItemBackgroundBorderless" android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/create_collection_close" android:contentDescription="@string/create_collection_close"
android:tint="@color/primary_text_dark_theme" app:tint="@color/primary_text_dark_theme"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_close" /> app:srcCompat="@drawable/ic_close" />

@ -11,7 +11,7 @@
android:background="@drawable/collection_home_list_row_background" android:background="@drawable/collection_home_list_row_background"
android:clickable="true" android:clickable="true"
android:clipToPadding="false" android:clipToPadding="false"
android:elevation="5dp" android:elevation="@dimen/home_collection_elevation"
android:focusable="true" android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"> android:foreground="?android:attr/selectableItemBackground">

@ -29,7 +29,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_tab_collection" app:srcCompat="@drawable/ic_tab_collection"
android:tint="@null" app:tint="@null"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

@ -14,53 +14,55 @@
android:id="@+id/back_button" android:id="@+id/back_button"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:drawablePadding="8dp"
android:gravity="start|center_vertical"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingTop="16dp" android:paddingTop="16dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingBottom="16dp" android:paddingBottom="16dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:drawableStartCompat="@drawable/mozac_ic_back"
android:drawablePadding="8dp"
app:drawableTint="@color/neutral_text"
android:gravity="start|center_vertical"
android:text="@string/create_collection_select_tabs" android:text="@string/create_collection_select_tabs"
android:textAppearance="@style/HeaderTextStyle" android:textAppearance="@style/HeaderTextStyle"
android:textColor="@color/neutral_text" android:textColor="@color/neutral_text"
android:textSize="20sp" android:textSize="20sp"
app:drawableStartCompat="@drawable/mozac_ic_back"
app:drawableTint="@color/neutral_text"
app:layout_constraintEnd_toStartOf="@+id/guideline" app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap"/> app:layout_constraintWidth_default="wrap"
tools:ignore="ButtonStyleXmlDetector" />
<Button <Button
android:id="@+id/select_all_button" android:id="@+id/select_all_button"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:fontFamily="@font/metropolis_bold"
android:gravity="start|center_vertical"
android:paddingStart="8dp" android:paddingStart="8dp"
android:paddingTop="16dp" android:paddingTop="16dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:paddingBottom="16dp" android:paddingBottom="16dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:gravity="start|center_vertical"
android:text="@string/create_collection_select_all" android:text="@string/create_collection_select_all"
android:textAllCaps="false" android:textAllCaps="false"
android:textColor="@color/neutral_text" android:textColor="@color/neutral_text"
android:textSize="16sp" android:textSize="16sp"
android:fontFamily="@font/metropolis_bold" app:layout_constraintBottom_toBottomOf="@id/back_button"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1" app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@+id/guideline" app:layout_constraintStart_toEndOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/back_button" app:layout_constraintWidth_default="wrap"
app:layout_constraintWidth_default="wrap"/> tools:ignore="ButtonStyleXmlDetector" />
<androidx.constraintlayout.widget.Guideline <androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline" android:id="@+id/guideline"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
app:layout_constraintGuide_percent="0.5"/> app:layout_constraintGuide_percent="0.5" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/collections_list" android:id="@+id/collections_list"
@ -106,8 +108,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="102dp" android:layout_height="102dp"
android:background="@drawable/simple_dark_grey_gradient" android:background="@drawable/simple_dark_grey_gradient"
app:layout_constraintBottom_toBottomOf="parent" android:focusable="false"
android:focusable="false"/> app:layout_constraintBottom_toBottomOf="parent" />
<EditText <EditText
android:id="@+id/name_collection_edittext" android:id="@+id/name_collection_edittext"
@ -117,6 +119,7 @@
android:autofillHints="false" android:autofillHints="false"
android:background="?foundation" android:background="?foundation"
android:focusedByDefault="true" android:focusedByDefault="true"
android:hint="@string/collection_name_hint"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:importantForAutofill="no" android:importantForAutofill="no"
android:inputType="textCapSentences" android:inputType="textCapSentences"
@ -136,9 +139,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
android:foreground="@drawable/rounded_ripple"
android:background="@drawable/add_tabs_to_collection_background" android:background="@drawable/add_tabs_to_collection_background"
android:clipToPadding="false" android:clipToPadding="false"
android:foreground="@drawable/rounded_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"> app:layout_constraintStart_toStartOf="parent">
@ -147,24 +150,24 @@
android:id="@+id/bottom_bar_icon_button" android:id="@+id/bottom_bar_icon_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="16dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/create_collection_close" android:contentDescription="@string/create_collection_close"
app:srcCompat="@drawable/mozac_ic_close" android:padding="16dp"
android:tint="?neutral"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/mozac_ic_close"
app:tint="?neutral" />
<TextView <TextView
android:id="@+id/bottom_bar_text" android:id="@+id/bottom_bar_text"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:fontFamily="@font/metropolis_semibold"
android:gravity="center_vertical" android:gravity="center_vertical"
android:text="@string/create_collection_save_to_collection_empty" android:text="@string/create_collection_save_to_collection_empty"
android:textColor="?neutral" android:textColor="?neutral"
android:textSize="16sp" android:textSize="16sp"
android:fontFamily="@font/metropolis_semibold"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/save_button" app:layout_constraintEnd_toStartOf="@id/save_button"
app:layout_constraintStart_toEndOf="@id/bottom_bar_icon_button" app:layout_constraintStart_toEndOf="@id/bottom_bar_icon_button"

@ -16,16 +16,17 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_margin="16dp" android:layout_margin="16dp"
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="?android:attr/selectableItemBackgroundBorderless"
app:drawableStartCompat="@drawable/mozac_ic_back"
android:drawablePadding="8dp" android:drawablePadding="8dp"
app:drawableTint="@color/neutral_text"
android:gravity="start|center_vertical" android:gravity="start|center_vertical"
android:text="@string/create_collection_name_collection" android:text="@string/create_collection_name_collection"
android:textAppearance="@style/HeaderTextStyle" android:textAppearance="@style/HeaderTextStyle"
android:textColor="@color/neutral_text" android:textColor="@color/neutral_text"
android:textSize="20sp" android:textSize="20sp"
app:drawableStartCompat="@drawable/mozac_ic_back"
app:drawableTint="@color/neutral_text"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
tools:ignore="ButtonStyleXmlDetector" />
<Button <Button
android:id="@+id/select_all_button" android:id="@+id/select_all_button"
@ -40,7 +41,8 @@
android:textSize="16sp" android:textSize="16sp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
tools:ignore="ButtonStyleXmlDetector" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/collections_list" android:id="@+id/collections_list"
@ -60,6 +62,7 @@
android:autofillHints="false" android:autofillHints="false"
android:background="?foundation" android:background="?foundation"
android:focusedByDefault="true" android:focusedByDefault="true"
android:hint="@string/collection_name_hint"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:importantForAutofill="no" android:importantForAutofill="no"
android:inputType="textCapSentences" android:inputType="textCapSentences"
@ -121,11 +124,11 @@
android:layout_margin="16dp" android:layout_margin="16dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/create_collection_close" android:contentDescription="@string/create_collection_close"
app:srcCompat="@drawable/mozac_ic_close"
android:tint="?neutral"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/mozac_ic_close"
app:tint="?neutral" />
<TextView <TextView
android:id="@+id/bottom_bar_text" android:id="@+id/bottom_bar_text"
@ -154,6 +157,7 @@
android:textColor="?neutral" android:textColor="?neutral"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
tools:ignore="ButtonStyleXmlDetector" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

@ -16,16 +16,17 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_margin="16dp" android:layout_margin="16dp"
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="?android:attr/selectableItemBackgroundBorderless"
app:drawableStartCompat="@drawable/mozac_ic_back"
android:drawablePadding="8dp" android:drawablePadding="8dp"
android:gravity="start|center_vertical" android:gravity="start|center_vertical"
app:drawableTint="@color/neutral_text"
android:text="@string/create_collection_select_collection" android:text="@string/create_collection_select_collection"
android:textAppearance="@style/HeaderTextStyle" android:textAppearance="@style/HeaderTextStyle"
android:textColor="@color/neutral_text" android:textColor="@color/neutral_text"
android:textSize="20sp" android:textSize="20sp"
app:drawableStartCompat="@drawable/mozac_ic_back"
app:drawableTint="@color/neutral_text"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
tools:ignore="ButtonStyleXmlDetector" />
<Button <Button
android:id="@+id/select_all_button" android:id="@+id/select_all_button"
@ -40,7 +41,8 @@
android:textSize="16sp" android:textSize="16sp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
tools:ignore="ButtonStyleXmlDetector" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/collections_list" android:id="@+id/collections_list"
@ -64,6 +66,7 @@
android:autofillHints="false" android:autofillHints="false"
android:background="?foundation" android:background="?foundation"
android:focusedByDefault="true" android:focusedByDefault="true"
android:hint="@string/collection_name_hint"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:importantForAutofill="no" android:importantForAutofill="no"
android:inputType="textCapSentences" android:inputType="textCapSentences"
@ -124,12 +127,12 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
app:srcCompat="@drawable/ic_new"
android:tint="?neutral"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_new"
app:tint="?neutral" />
<TextView <TextView
android:id="@+id/bottom_bar_text" android:id="@+id/bottom_bar_text"
@ -137,10 +140,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:fontFamily="@font/metropolis"
android:gravity="center_vertical" android:gravity="center_vertical"
android:singleLine="true" android:singleLine="true"
android:text="@string/create_collection_add_new_collection" android:text="@string/create_collection_add_new_collection"
android:fontFamily="@font/metropolis"
android:textColor="?neutral" android:textColor="?neutral"
android:textSize="16sp" android:textSize="16sp"
android:textStyle="bold" android:textStyle="bold"
@ -161,6 +164,7 @@
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
tools:ignore="ButtonStyleXmlDetector" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

@ -25,6 +25,7 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
tools:ignore="MozMultipleConstraintLayouts"
android:id="@+id/infoBanner" android:id="@+id/infoBanner"
android:visibility="gone" android:visibility="gone"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -48,6 +49,7 @@
app:layout_constraintBottom_toTopOf="@id/infoBanner" /> app:layout_constraintBottom_toTopOf="@id/infoBanner" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
tools:ignore="MozMultipleConstraintLayouts"
android:id="@+id/topBar" android:id="@+id/topBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="50dp" android:layout_height="50dp"

@ -25,6 +25,7 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
tools:ignore="MozMultipleConstraintLayouts"
android:id="@+id/infoBanner" android:id="@+id/infoBanner"
android:visibility="gone" android:visibility="gone"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -49,6 +50,7 @@
app:layout_constraintTop_toBottomOf="@id/infoBanner" /> app:layout_constraintTop_toBottomOf="@id/infoBanner" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
tools:ignore="MozMultipleConstraintLayouts"
android:id="@+id/topBar" android:id="@+id/topBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="50dp" android:layout_height="50dp"

@ -25,6 +25,7 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
tools:ignore="MozMultipleConstraintLayouts"
android:id="@+id/infoBanner" android:id="@+id/infoBanner"
android:visibility="gone" android:visibility="gone"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -49,6 +50,7 @@
app:layout_constraintTop_toBottomOf="@id/infoBanner" /> app:layout_constraintTop_toBottomOf="@id/infoBanner" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
tools:ignore="MozMultipleConstraintLayouts"
android:id="@+id/topBar" android:id="@+id/topBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="80dp" android:layout_height="80dp"

@ -25,6 +25,7 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
tools:ignore="MozMultipleConstraintLayouts"
android:id="@+id/infoBanner" android:id="@+id/infoBanner"
android:visibility="gone" android:visibility="gone"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -48,6 +49,7 @@
app:layout_constraintBottom_toTopOf="@id/infoBanner" /> app:layout_constraintBottom_toTopOf="@id/infoBanner" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
tools:ignore="MozMultipleConstraintLayouts"
android:id="@+id/topBar" android:id="@+id/topBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="80dp" android:layout_height="80dp"

@ -18,10 +18,10 @@
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:scaleType="center" android:scaleType="center"
android:tint="?primaryText"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/mozac_feature_download_ic_download_complete" /> app:srcCompat="@drawable/mozac_feature_download_ic_download_complete"
app:tint="?primaryText" />
<TextView <TextView
android:id="@+id/download_dialog_title" android:id="@+id/download_dialog_title"
@ -48,10 +48,10 @@
android:layout_height="50dp" android:layout_height="50dp"
android:background="@null" android:background="@null"
android:contentDescription="@string/mozac_feature_downloads_button_close" android:contentDescription="@string/mozac_feature_downloads_button_close"
android:tint="?primaryText"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/mozac_ic_close" /> app:srcCompat="@drawable/mozac_ic_close"
app:tint="?primaryText" />
<TextView <TextView
android:id="@+id/download_dialog_filename" android:id="@+id/download_dialog_filename"
@ -69,6 +69,7 @@
<Button <Button
android:id="@+id/download_dialog_action_button" android:id="@+id/download_dialog_action_button"
style="@style/PositiveButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
@ -76,9 +77,9 @@
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:background="@drawable/rounded_all_corners" android:background="@drawable/rounded_all_corners"
android:backgroundTint="?accent" android:backgroundTint="?accent"
android:padding="16dp"
android:text="@string/mozac_feature_downloads_button_open" android:text="@string/mozac_feature_downloads_button_open"
android:textAllCaps="false" android:textAllCaps="false"
android:padding="16dp"
android:textColor="?contrastText" android:textColor="?contrastText"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

@ -5,9 +5,11 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<mozilla.components.concept.engine.EngineView <mozilla.components.concept.engine.EngineView
tools:ignore="Instantiatable"
android:id="@+id/addonSettingsEngineView" android:id="@+id/addonSettingsEngineView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />

@ -30,6 +30,7 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<mozilla.components.concept.engine.EngineView <mozilla.components.concept.engine.EngineView
tools:ignore="Instantiatable"
android:id="@+id/engineView" android:id="@+id/engineView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

@ -55,6 +55,7 @@
app:layout_constraintWidth_percent="0.9" /> app:layout_constraintWidth_percent="0.9" />
<Button <Button
style="@style/PositiveButton"
android:id="@+id/restoreTabButton" android:id="@+id/restoreTabButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="48dp" android:layout_height="48dp"
@ -66,7 +67,6 @@
android:fontFamily="Sharp Sans" android:fontFamily="Sharp Sans"
android:text="@string/tab_crash_restore" android:text="@string/tab_crash_restore"
android:textAllCaps="false" android:textAllCaps="false"
android:textColor="?contrastText"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -74,6 +74,7 @@
app:layout_constraintWidth_percent="0.4" /> app:layout_constraintWidth_percent="0.4" />
<Button <Button
style="@style/NeutralButton"
android:id="@+id/closeTabButton" android:id="@+id/closeTabButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="48dp" android:layout_height="48dp"

@ -26,7 +26,7 @@
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_tab_collection" app:srcCompat="@drawable/ic_tab_collection"
android:tint="@null" app:tint="@null"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="parent"/> app:layout_constraintTop_toBottomOf="parent"/>

@ -25,7 +25,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="24dp" android:layout_marginStart="24dp"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:autofillHints="false"
android:backgroundTint="?neutral" android:backgroundTint="?neutral"
android:hint="@string/collection_name_hint"
android:inputType="textCapSentences" android:inputType="textCapSentences"
android:singleLine="true" android:singleLine="true"
android:textAlignment="viewStart" /> android:textAlignment="viewStart" />

@ -23,6 +23,7 @@
<Button <Button
android:id="@+id/fxa_sign_in_button" android:id="@+id/fxa_sign_in_button"
style="@style/NeutralButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
@ -37,5 +38,4 @@
android:textSize="14sp" android:textSize="14sp"
android:textStyle="bold" android:textStyle="bold"
app:backgroundTint="@color/onboarding_card_button_background_dark" /> app:backgroundTint="@color/onboarding_card_button_background_dark" />
</LinearLayout> </LinearLayout>

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

Loading…
Cancel
Save