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
*Application.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
# about to be enabled.

@ -6,6 +6,9 @@ labels: "\U0001F41E bug"
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
@ -17,3 +20,4 @@ assignees: ''
* Android device: ?
* Fenix version: ?

@ -6,6 +6,10 @@ labels: "🌟 feature request"
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?

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

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

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

@ -1,11 +1,14 @@
language: android
dist: trusty
script:
# Prepare SDK
- sudo mkdir -p /usr/local/android-sdk/licenses/
- sudo touch /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 "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
- ./gradlew -q testDebug 2>&1
# Make sure a release build builds

@ -31,7 +31,7 @@ android {
resValue "bool", "IS_DEBUG", "false"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "false"
buildConfigField "String", "AMO_ACCOUNT", "\"mozilla\""
buildConfigField "String", "AMO_COLLECTION", "\"83a9cccfe6e24a34bd7b155ff9ee32\""
buildConfigField "String", "AMO_COLLECTION", "\"7dfae8669acc4312a65e8ba5553036\""
def deepLinkSchemeValue = "fenix-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
@ -59,14 +59,12 @@ android {
shrinkResources false
minifyEnabled false
applicationIdSuffix ".fenix.debug"
buildConfigField "String", "AMO_COLLECTION", "\"7dfae8669acc4312a65e8ba5553036\""
resValue "bool", "IS_DEBUG", "true"
pseudoLocalesEnabled true
}
nightly releaseTemplate >> {
applicationIdSuffix ".fenix"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
buildConfigField "String", "AMO_COLLECTION", "\"7dfae8669acc4312a65e8ba5553036\""
def deepLinkSchemeValue = "fenix-nightly"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = ["deepLinkScheme": deepLinkSchemeValue]
@ -199,6 +197,7 @@ android {
lintOptions {
lintConfig file("lint.xml")
baseline file("lint-baseline.xml")
}
packagingOptions {
@ -355,7 +354,31 @@ ext.gleanGenerateMarkdownDocs = true
ext.gleanDocsDirectory = "$rootDir/docs"
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 {
jnaForTest Deps.jna
testImplementation files(configurations.jnaForTest.copyRecursive().files)
debugImplementation 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.google_material
implementation Deps.lottie
androidTestImplementation Deps.uiautomator
// Removed pending AndroidX fixes
androidTestImplementation "tools.fastlane:screengrab:2.0.0"
@ -577,13 +597,13 @@ if (project.hasProperty("coverage")) {
// -------------------------------------------------------------------------------------------------
tasks.register('printVariants') {
doLast {
def variants = android.applicationVariants.collect {[
apks: it.variantData.outputScope.apkDatas.collect {[
abi: it.filters.find { it.filterType == 'ABI' }.identifier,
fileName: it.outputFileName,
]},
build_type: it.buildType.name,
name: it.name,
def variants = android.applicationVariants.collect { variant -> [
apks: variant.outputs.collect { output -> [
abi: output.getFilter(com.android.build.VariantOutput.FilterType.ABI),
fileName: output.outputFile.name
]},
build_type: variant.buildType.name,
name: variant.name,
]}
// AndroidTest is a special case not included above
variants.add([
@ -605,8 +625,8 @@ task buildTranslationArray {
foundLocales.append("new String[]{")
fileTree("src/main/res").visit { FileVisitDetails details ->
if(details.file.path.endsWith("/strings.xml")){
def languageCode = details.file.parent.tokenize('/').last().replaceAll('values-','').replaceAll('-r','-')
if(details.file.path.endsWith("${File.separator}strings.xml")){
def languageCode = details.file.parent.tokenize(File.separator).last().replaceAll('values-','').replaceAll('-r','-')
languageCode = (languageCode == "values") ? "en-US" : languageCode
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.
Let's just ignore issues in the Sentry code since that is a third-party dependency anyways. -->
<ignore path="**/sentry*.jar" />
<!-- Temporary until https://github.com/Kotlin/kotlinx.coroutines/issues/2004 is resolved. -->
<ignore path="**/kotlinx-coroutines-core-*.jar"/>
</issue>
<!-- Lints that don't apply to our translation process -->
<issue id="MissingTranslation" severity="ignore" />
<issue id="PluralsCandidate" severity="ignore" />
<issue id="StringFormatCount" severity="ignore" />
<issue id="TypographyEllipsis" severity="ignore" />
<issue id="ExtraTranslation" severity="warning" />
<!-- Lints that are disabled by default -->
<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>

@ -279,7 +279,7 @@ events:
whats_new_tapped:
type: event
description: |
A user opened the "what's new" page button
A user opened the "what's new" page button
bugs:
- https://github.com/mozilla-mobile/fenix/issues/5021
data_reviews:
@ -947,6 +947,40 @@ metrics:
notification_emails:
- fenix-core@mozilla.com
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:
type: boolean
lifetime: application
@ -4207,3 +4241,18 @@ master_password:
notification_emails:
- fenix-core@mozilla.com
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;
}
####################################################################################################
# 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
####################################################################################################

@ -15,7 +15,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
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.config.Configuration
import mozilla.components.service.glean.net.ConceptFetchHttpUploader
@ -78,7 +77,7 @@ class BaselinePingTest {
@JvmStatic
fun setupOnce() {
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

@ -10,9 +10,9 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.Ignore
import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
@ -85,7 +85,7 @@ class SettingsBasicsTest {
verifyThemes()
}.goBack {
}.openAccessibilitySubMenu {
verifyMenuItems()
verifyAutomaticFontSizingMenuItems()
}.goBack {
// drill down to submenu
}
@ -181,7 +181,7 @@ class SettingsBasicsTest {
}
@Test
fun changeAccessibilitySettings() {
fun changeAccessibiltySettings() {
// 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 webpage = getLoremIpsumAsset(mockWebServer).url
@ -193,7 +193,8 @@ class SettingsBasicsTest {
}.openThreeDotMenu {
}.openSettings {
}.openAccessibilitySubMenu {
verifyMenuItems()
clickFontSizingSwitch()
verifyEnabledMenuItems()
changeTextSizeSlider(textSizePercentage)
verifyTextSizePercentage(textSizePercentage)
}.goBack {
@ -201,6 +202,14 @@ class SettingsBasicsTest {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(webpage) {
checkTextSizeOnWebsite(textSizePercentage, fenixApp.components)
}.openTabDrawer {
}.openNewTab {
}.dismiss {
}.openThreeDotMenu {
}.openSettings {
}.openAccessibilitySubMenu {
clickFontSizingSwitch()
verifyMenuItemsAreDisabled()
}
}

@ -311,8 +311,10 @@ class TabbedBrowsingTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabButtonShortcutsMenu {
}.openNewPrivateTabFromShortcutsMenu {
verifyHomeScreen()
verifyNavigationToolbar()
verifyKeyboardVisible()
verifyFocusedNavigationToolbar()
// dismiss search dialog
homeScreen { }.pressBack()
verifyHomePrivateBrowsingButton()
verifyHomeMenu()
verifyHomeWordmark()
@ -326,8 +328,10 @@ class TabbedBrowsingTest {
}.openTabButtonShortcutsMenu {
}.openTabFromShortcutsMenu {
verifyHomeScreen()
verifyNavigationToolbar()
verifyKeyboardVisible()
verifyFocusedNavigationToolbar()
// dismiss search dialog
homeScreen { }.pressBack()
verifyHomeMenu()
verifyHomeWordmark()
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.withContentDescription
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.withText
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.containsString
import org.hamcrest.CoreMatchers.not
import org.junit.Assert
import org.mozilla.fenix.R
import org.mozilla.fenix.components.Search
import org.mozilla.fenix.helpers.TestAssetHelper
@ -52,6 +54,7 @@ import org.mozilla.fenix.helpers.withBitmapDrawable
*/
class HomeScreenRobot {
fun verifyNavigationToolbar() = assertNavigationToolbar()
fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar()
fun verifyHomeScreen() = assertHomeScreen()
fun verifyHomePrivateBrowsingButton() = assertHomePrivateBrowsingButton()
fun verifyHomeMenu() = assertHomeMenu()
@ -64,6 +67,7 @@ class HomeScreenRobot {
fun verifyHomeComponent() = assertHomeComponent()
fun verifyDefaultSearchEngine(searchEngine: String) = verifySearchEngineIcon(searchEngine)
fun verifyNoTabsOpened() = assertNoTabsOpened()
fun verifyKeyboardVisible() = assertKeyboardVisibility(isExpectedToBeVisible = true)
// First Run elements
fun verifyWelcomeHeader() = assertWelcomeHeader()
@ -387,6 +391,10 @@ class HomeScreenRobot {
.perform(click())
}
fun pressBack() {
onView(ViewMatchers.isRoot()).perform(ViewActions.pressBack())
}
fun openTabsListThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
// tabsListThreeDotButton().perform(click())
@ -481,6 +489,14 @@ fun homeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
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() =
onView(allOf(withText("Search or enter address")))
@ -490,6 +506,10 @@ private fun assertNavigationToolbar() =
onView(allOf(withText("Search or enter address")))
.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"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

@ -6,30 +6,32 @@
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.KEYCODE_DPAD_RIGHT
import android.view.KeyEvent.KEYCODE_DPAD_LEFT
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.widget.SeekBar
import android.widget.TextView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewAssertion
import org.hamcrest.CoreMatchers.allOf
import androidx.test.espresso.matcher.ViewMatchers.withText
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.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.Visibility
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.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.MIN_VALUE
import org.mozilla.fenix.ui.robots.SettingsSubMenuAccessibilityRobot.Companion.STEP_SIZE
@ -48,7 +50,13 @@ class SettingsSubMenuAccessibilityRobot {
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)
@ -69,7 +77,22 @@ class SettingsSubMenuAccessibilityRobot {
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()
assertSliderBar()
}
@ -77,9 +100,11 @@ private fun assertMenuItems() {
private fun assertFontSize() {
val view = onView(withText("Font Size"))
view.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(isEnabled(true)))
val strFont = "Make text on websites larger or smaller"
onView(withText(strFont))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(isEnabled(true)))
}
private fun assertSliderBar() {
@ -103,6 +128,20 @@ private fun assertTextSizePercentage(textSize: Int) {
.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() =
onView(allOf(withContentDescription("Navigate up")))

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

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

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

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

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

@ -53,6 +53,10 @@ private const val DEFAULT_VALUE = Int.MAX_VALUE
/**
* 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() {
private val scope = CoroutineScope(Dispatchers.IO)
@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.
*/
@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(
private val addonCollectionProvider: PagedAddonCollectionProvider,
private val addonsManagerDelegate: AddonsManagerAdapterDelegate,

@ -21,11 +21,6 @@ object FeatureFlags {
*/
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.
*/
@ -50,4 +45,9 @@ object FeatureFlags {
* Enables ETP cookie purging
*/
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.support.base.log.Log
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.runOnlyInMainProcess
import mozilla.components.support.locale.LocaleAwareApplication
@ -42,7 +41,6 @@ import mozilla.components.support.utils.logElapsedTime
import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StorageStatsMetrics
import org.mozilla.fenix.perf.StartupTimeline
@ -114,7 +112,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
setupCrashReporting()
// We want the log messages of all builds to go to Android logcat
Log.addSink(AndroidLogSink())
Log.addSink(FenixLogSink(logsDebug = Config.channel.isDebug))
}
@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.launch
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.WebExtensionState
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.addons.AddonDetailsFragmentDirections
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.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
@ -122,11 +121,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
private var webExtScope: CoroutineScope? = null
lateinit var themeManager: ThemeManager
lateinit var browsingModeManager: BrowsingModeManager
private lateinit var sessionObserver: SessionManager.Observer
private var isVisuallyComplete = false
private var privateNotificationObserver: PrivateNotificationFeature<PrivateNotificationService>? = null
private var privateNotificationObserver: PrivateNotificationFeature<PrivateNotificationService>? =
null
private var isToolbarInflated = false
@ -181,8 +180,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
.attachViewToRunVisualCompletenessQueueLater(WeakReference(rootContainer))
}
sessionObserver = UriOpenedObserver(this)
checkPrivateShortcutEntryPoint(intent)
privateNotificationObserver = PrivateNotificationFeature(
applicationContext,
@ -192,14 +189,18 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
it.start()
}
if (isActivityColdStarted(intent, savedInstanceState)) {
externalSourceIntentProcessors.any {
if (isActivityColdStarted(
intent,
savedInstanceState
) && !externalSourceIntentProcessors.any {
it.process(
intent,
navHost.navController,
this.intent
)
}
) {
navigateToBrowserOnColdStart()
}
Performance.processIntentIfPerformanceTest(intent, this)
@ -240,7 +241,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
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(
safeIntent,
hasSavedInstanceState,
@ -322,10 +326,18 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
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) {
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()
// 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) {
base.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
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.MainScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
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.selector.findTab
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.content.DownloadState
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 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)
browserFragmentStore = StoreProvider.get(this) {
@ -198,8 +222,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
)
}
components = requireComponents
return view
}

@ -11,7 +11,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
@ -86,7 +85,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
val readerModeAction =
BrowserToolbar.ToggleButton(
image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_readermode)!!,
image = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode)!!,
imageSelected =
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode_selected)!!,
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.widget.Button
import android.widget.RadioButton
import androidx.core.content.ContextCompat
import androidx.appcompat.content.res.AppCompatResources
import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.R
@ -54,7 +54,7 @@ class DefaultReaderModeController(
findViewById<Button>(it)
}.forEach {
it.setTextColor(
ContextCompat.getColorStateList(
AppCompatResources.getColorStateList(
context,
R.color.readerview_private_button_color
)
@ -68,7 +68,7 @@ class DefaultReaderModeController(
findViewById<RadioButton>(it)
}.forEach {
it.setTextColor(
ContextCompat.getColorStateList(
AppCompatResources.getColorStateList(
context,
R.color.readerview_private_radio_color
)

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

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

@ -15,6 +15,7 @@ import mozilla.components.feature.downloads.DownloadsUseCases
import mozilla.components.feature.pwa.WebAppShortcutManager
import mozilla.components.feature.pwa.WebAppUseCases
import mozilla.components.feature.search.SearchUseCases
import mozilla.components.browser.search.ext.toDefaultSearchEngineProvider
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.session.SettingsUseCases
import mozilla.components.feature.session.TrackingProtectionUseCases
@ -51,7 +52,14 @@ class UseCases(
/**
* 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.

@ -194,6 +194,8 @@ sealed class Event {
object MasterPasswordMigrationSuccess : Event()
object MasterPasswordMigrationDisplayed : Event()
object TabSettingsOpened : Event()
// Interaction events with extras
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.SyncAuth
import org.mozilla.fenix.GleanMetrics.Tab
import org.mozilla.fenix.GleanMetrics.Tabs
import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.GleanMetrics.Tip
import org.mozilla.fenix.GleanMetrics.ToolbarSettings
@ -705,6 +706,9 @@ private val Event.wrapper: EventWrapper<*>?
Event.MasterPasswordMigrationSuccess -> EventWrapper<NoExtraKeys>(
{ MasterPassword.migration.record(it) }
)
Event.TabSettingsOpened -> EventWrapper<NoExtraKeys>(
{ Tabs.settingOpened.record(it) }
)
// Don't record other events in Glean:
is Event.AddBookmark -> null
@ -719,7 +723,11 @@ private val Event.wrapper: EventWrapper<*>?
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
private val logger = Logger("GleanMetricsService")
@ -754,12 +762,12 @@ class GleanMetricsService(private val context: Context) : MetricsService {
internal fun setStartupMetrics() {
setPreferenceMetrics()
Metrics.apply {
defaultBrowser.set(BrowsersCache.all(context).isDefaultBrowser)
MozillaProductDetector.getMozillaBrowserDefault(context)?.also {
with(Metrics) {
defaultBrowser.set(browsersCache.all(context).isDefaultBrowser)
mozillaProductDetector.getMozillaBrowserDefault(context)?.also {
defaultMozBrowser.set(it)
}
mozillaProducts.set(MozillaProductDetector.getInstalledMozillaProducts(context))
mozillaProducts.set(mozillaProductDetector.getInstalledMozillaProducts(context))
adjustCampaign.set(context.settings().adjustCampaignId)
adjustAdGroup.set(context.settings().adjustAdGroup)
@ -786,6 +794,9 @@ class GleanMetricsService(private val context: Context) : MetricsService {
ToolbarPosition.TOP -> Event.ToolbarPositionChanged.Position.TOP.name
}
)
tabViewSetting.set(context.settings().getTabViewPingString())
closeTabSetting.set(context.settings().getTabTimeoutPingString())
}
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
// 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.apply {
with(Preferences) {
showSearchSuggestions.set(context.settings().shouldShowSearchSuggestions.toStringList())
remoteDebugging.set(context.settings().isRemoteDebuggingEnabled.toStringList())
telemetry.set(context.settings().isTelemetryEnabled.toStringList())
@ -859,7 +870,9 @@ class GleanMetricsService(private val context: Context) : MetricsService {
val accessibilitySelection = mutableListOf<String>()
if (context.settings().switchServiceIsEnabled) { accessibilitySelection.add("switch") }
if (context.settings().switchServiceIsEnabled) {
accessibilitySelection.add("switch")
}
if (context.settings().touchExplorationIsEnabled) {
accessibilitySelection.add("touch exploration")

@ -33,7 +33,7 @@ import java.util.Locale
open class FenixSearchEngineProvider(
private val context: Context
) : 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) {
LocationService.dummy()
} else {
@ -55,8 +55,12 @@ open class FenixSearchEngineProvider(
open val localizationProvider: SearchLocalizationProvider =
RegionSearchLocalizationProvider(locationService)
/**
* Unfiltered list of search engines based on locale.
*/
open var baseSearchEngines = async {
AssetsSearchEngineProvider(localizationProvider).loadSearchEngines(context)
AssetsSearchEngineProvider(localizationProvider)
.loadSearchEngines(context)
}
private val loadedRegion = async { localizationProvider.determineRegion() }
@ -72,9 +76,13 @@ open class FenixSearchEngineProvider(
open val fallbackEngines = async { fallBackProvider.loadSearchEngines(context) }
private val fallbackRegion = async { fallbackLocationService.determineRegion() }
/**
* Default bundled search engines based on locale.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
open val bundledSearchEngines = async {
val defaultEngineIdentifiers = baseSearchEngines.await().list.map { it.identifier }.toSet()
val defaultEngineIdentifiers =
baseSearchEngines.await().list.map { it.identifier }.toSet()
AssetsSearchEngineProvider(
localizationProvider,
filters = listOf(object : SearchEngineFilter {
@ -87,12 +95,15 @@ open class FenixSearchEngineProvider(
).loadSearchEngines(context)
}
/**
* Search engines that have been manually added by a user.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
open var customSearchEngines = async {
CustomSearchEngineProvider().loadSearchEngines(context)
}
private var loadedSearchEngines = refreshAsync(baseSearchEngines)
private var loadedSearchEngines = refreshInstalledEngineListAsync(baseSearchEngines)
// https://github.com/mozilla-mobile/fenix/issues/9935
// Create new getter that will return the fallback SearchEngineList if
@ -102,29 +113,45 @@ open class FenixSearchEngineProvider(
if (isRegionCachedByLocationService || shouldMockMLS) {
loadedSearchEngines
} else {
refreshAsync(fallbackEngines)
refreshInstalledEngineListAsync(fallbackEngines)
}
fun getDefaultEngine(context: Context): SearchEngine {
val engines = installedSearchEngines(context)
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
* 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 {
val installedIdentifiers = installedSearchEngineIdentifiers(context)
val engineList = searchEngines.await()
val defaultList = searchEngines.await()
engineList.copy(
list = engineList.list.filter {
defaultList.copy(
list = defaultList.list.filter {
installedIdentifiers.contains(it.identifier)
}.sortedBy { it.name.toLowerCase(Locale.getDefault()) },
default = engineList.default?.let {
}.sortedBy {
it.name.toLowerCase(Locale.getDefault())
},
default = defaultList.default?.let {
if (installedIdentifiers.contains(it.identifier)) {
it
} else {
@ -142,14 +169,20 @@ open class FenixSearchEngineProvider(
val installedIdentifiers = installedSearchEngineIdentifiers(context)
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 {
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) {
val searchUrl = searchEngine.getSearchTemplate()
CustomSearchEngineStore.addSearchEngine(context, searchEngine.name, searchUrl)
@ -158,25 +191,34 @@ open class FenixSearchEngineProvider(
val installedIdentifiers = installedSearchEngineIdentifiers(context).toMutableSet()
installedIdentifiers.add(searchEngine.identifier)
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) {
CustomSearchEngineStore.removeSearchEngine(context, searchEngine.identifier)
reload()
} else {
val installedIdentifiers = installedSearchEngineIdentifiers(context).toMutableSet()
installedIdentifiers.remove(searchEngine.identifier)
prefs(context).edit().putStringSet(localeAwareInstalledEnginesKey(), installedIdentifiers).apply()
prefs(context).edit().putStringSet(
localeAwareInstalledEnginesKey(),
installedIdentifiers
).apply()
}
}
fun reload() {
launch {
customSearchEngines = async { CustomSearchEngineProvider().loadSearchEngines(context) }
loadedSearchEngines = refreshAsync(baseSearchEngines)
loadedSearchEngines = refreshInstalledEngineListAsync(baseSearchEngines)
}
}
@ -184,16 +226,19 @@ open class FenixSearchEngineProvider(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
open fun updateBaseSearchEngines() {
baseSearchEngines = async {
AssetsSearchEngineProvider(localizationProvider).loadSearchEngines(context)
AssetsSearchEngineProvider(localizationProvider)
.loadSearchEngines(context)
}
}
private fun refreshAsync(baseList: Deferred<SearchEngineList>) = async {
val engineList = baseList.await()
private fun refreshInstalledEngineListAsync(
engines: Deferred<SearchEngineList>
): Deferred<SearchEngineList> = async {
val engineList = engines.await()
val bundledList = bundledSearchEngines.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(
@ -208,8 +253,11 @@ open class FenixSearchEngineProvider(
if (!prefs.contains(installedEnginesKey)) {
val searchEngines =
if (isRegionCachedByLocationService) baseSearchEngines
else fallbackEngines
if (isRegionCachedByLocationService) {
baseSearchEngines
} else {
fallbackEngines
}
val defaultSet = searchEngines.await()
.list
@ -219,9 +267,12 @@ open class FenixSearchEngineProvider(
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
}
@ -251,6 +302,5 @@ open class FenixSearchEngineProvider(
val BUNDLED_SEARCH_ENGINES = listOf("reddit", "youtube")
const val PREF_FILE_SEARCH_ENGINES = "fenix-search-engine-provider"
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.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputEditText
@ -73,7 +73,7 @@ class MasterPasswordTipProvider(
title = context.getString(R.string.mp_homescreen_tip_title),
description = context.getString(R.string.mp_homescreen_tip_message),
learnMoreURL = null,
titleDrawable = ContextCompat.getDrawable(context, R.drawable.ic_login)
titleDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_login)
)
private fun showMasterPasswordMigration() {

@ -124,7 +124,7 @@ class DefaultBrowserToolbarController(
}
is TabCounterMenu.Item.NewTab -> {
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.BrowserMenuImageText
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.SessionManager
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.storage.BookmarksStorage
import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature
import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
@ -188,6 +190,7 @@ class DefaultToolbarMenu(
settings,
if (shouldDeleteDataOnQuit) deleteDataOnQuit else null,
BrowserMenuDivider(),
reportSiteIssuePlaceholder,
findInPage,
addToTopSites,
addToHomescreen.apply { visible = ::canAddToHomescreen },
@ -283,6 +286,10 @@ class DefaultToolbarMenu(
onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
}
private val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem(
id = WebCompatReporterFeature.WEBCOMPAT_REPORTER_EXTENSION_ID
)
private val saveToCollection = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_save_to_collection_2),
imageResource = R.drawable.ic_tab_collection,

@ -39,7 +39,7 @@ class TabCounter @JvmOverloads constructor(
counter_root.contentDescription = if (count == 1) {
context?.getString(R.string.open_tab_tray_single)
} 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
}
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
counter_text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
counter_text.setTypeface(null, Typeface.BOLD)

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

@ -25,6 +25,7 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getStringWithArgSafe
import org.mozilla.fenix.ext.settings
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.
@ -121,7 +122,11 @@ class CustomTabToolbarMenu(
BrowserMenuDivider(),
menuToolbar
)
if (shouldReverseItems) { menuItems.reversed() } else { menuItems }
if (shouldReverseItems) {
menuItems.reversed()
} else {
menuItems
}
}
private val desktopMode = BrowserMenuImageSwitch(
@ -161,7 +166,8 @@ class CustomTabToolbarMenu(
}
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,
textColorResource = primaryTextColor(),
textStyle = Typeface.NORMAL

@ -46,6 +46,10 @@ open class ExternalAppBrowserActivity : HomeActivity() {
)
}
override fun navigateToBrowserOnColdStart() {
// No-op for external app
}
override fun getNavDirections(
from: BrowserDirection,
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.toCustomTabConfig
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.toSafeIntent
import org.json.JSONException
import org.json.JSONObject
import org.mozilla.fenix.R
import java.io.File
@ -40,6 +42,7 @@ class FennecWebAppIntentProcessor(
private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase,
private val storage: ManifestStorage
) : IntentProcessor {
val logger = Logger("FennecWebAppIntentProcessor")
/**
* Returns true if this intent should launch a progressive web app created in Fennec.
@ -113,6 +116,10 @@ class FennecWebAppIntentProcessor(
WebAppManifestParser().parse(manifestField).getOrNull()
} 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
}
}

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

@ -35,5 +35,5 @@ fun List<TabCollection>.getDefaultCollectionNumber(): Int {
.map { it.title }
.filter { it.matches(Regex("Collection\\s\\d+")) }
.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) {
lifecycleScope.launch(Main) {
view?.sessionControlRecyclerView?.adapter?.notifyDataSetChanged()
}
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 {
interactor.onCollectionOpenTabClicked(tab)
}

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

@ -21,11 +21,11 @@ import android.view.ViewStub
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintProperties.BOTTOM
import androidx.constraintlayout.widget.ConstraintProperties.PARENT_ID
import androidx.constraintlayout.widget.ConstraintProperties.TOP
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@ -431,7 +431,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
private fun addSearchButton(toolbarView: ToolbarView) {
toolbarView.view.addEditAction(
BrowserToolbar.Button(
ContextCompat.getDrawable(requireContext(), R.drawable.ic_microphone)!!,
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_microphone)!!,
requireContext().getString(R.string.voice_search_content_description),
visible = {
store.state.searchEngineSource.searchEngine.identifier.contains("google") &&
@ -481,8 +481,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
private fun updateClipboardSuggestion(searchState: SearchFragmentState, clipboardUrl: String?) {
val shouldShowView = searchState.showClipboardSuggestions &&
searchState.query.isEmpty() &&
!clipboardUrl.isNullOrEmpty() &&
!searchState.showSearchShortcuts
!clipboardUrl.isNullOrEmpty()
fill_link_from_clipboard.visibility = if (shouldShowView) View.VISIBLE else View.GONE
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.drawable.toBitmap
import mozilla.components.browser.awesomebar.BrowserAwesomeBar
import mozilla.components.browser.search.DefaultSearchEngineProvider
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
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.SessionSuggestionProvider
import mozilla.components.feature.search.SearchUseCases
import mozilla.components.browser.search.ext.toDefaultSearchEngineProvider
import mozilla.components.feature.syncedtabs.DeviceIndicators
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.syncedtabs.SyncedTabsStorageSuggestionProvider
@ -146,7 +148,9 @@ class AwesomeBarView(
defaultSearchSuggestionProvider =
SearchSuggestionProvider(
context = activity,
searchEngineManager = components.search.searchEngineManager,
defaultSearchEngineProvider = components.search.searchEngineManager.toDefaultSearchEngineProvider(
activity
),
searchUseCase = searchUseCase,
fetchClient = components.core.client,
mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
@ -159,9 +163,9 @@ class AwesomeBarView(
defaultSearchActionProvider =
SearchActionProvider(
searchEngineGetter = suspend {
components.search.searchEngineManager.getDefaultSearchEngineAsync(activity)
},
defaultSearchEngineProvider = components.search.searchEngineManager.toDefaultSearchEngineProvider(
activity
),
searchUseCase = searchUseCase,
icon = searchBitmap,
showDescription = false
@ -313,7 +317,11 @@ class AwesomeBarView(
listOf(
SearchActionProvider(
searchEngineGetter = suspend { searchEngine },
defaultSearchEngineProvider = object : DefaultSearchEngineProvider {
override fun getDefaultSearchEngine(): SearchEngine? = searchEngine
override suspend fun retrieveDefaultSearchEngine(): SearchEngine? =
searchEngine
},
searchUseCase = shortcutSearchUseCase,
icon = searchBitmap
),

@ -45,19 +45,38 @@ class AccessibilityFragment : PreferenceFragmentCompat() {
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
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
val useAutoSize = newTextScale == 1F
// Save new text scale value. We assume auto sizing is off if this change listener was called.
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.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) {
settings.fontSizeFactor = newTextScale
components.core.engine.settings.fontSizeFactor = newTextScale
components.core.engine.settings.fontSizeFactor = settings.fontSizeFactor
}
// Enable the manual sizing controls if automatic sizing is turned off.
textSizePreference.isEnabled = !useAutoSize
// Reload the current session to reflect the new text scale
components.useCases.sessionUseCases.reload()
true

@ -12,7 +12,6 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import org.mozilla.fenix.FeatureFlags
@ -219,12 +218,7 @@ class CustomizationFragment : PreferenceFragmentCompat() {
}
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 {
isVisible = FeatureFlags.topFrecentSite
isChecked = context.settings().showTopFrecentSites
onPreferenceChangeListener = SharedPreferenceUpdater()
}

@ -24,8 +24,7 @@ class DataChoicesFragment : PreferenceFragmentCompat() {
super.onCreate(savedInstanceState)
val context = requireContext()
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) {
_, key ->
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) { _, key ->
if (key == getPreferenceKey(R.string.pref_key_telemetry)) {
if (context.settings().isTelemetryEnabled) {
context.components.analytics.metrics.start(MetricServiceType.Data)
@ -63,7 +62,10 @@ class DataChoicesFragment : PreferenceFragmentCompat() {
isChecked = context.settings().isMarketingTelemetryEnabled
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()
}

@ -7,8 +7,6 @@ package org.mozilla.fenix.settings
import android.content.Context
import android.util.AttributeSet
import android.widget.ImageView
import androidx.core.content.res.TypedArrayUtils
import androidx.core.content.withStyledAttributes
import androidx.preference.PreferenceViewHolder
import org.mozilla.fenix.R
@ -32,29 +30,6 @@ class RadioButtonInfoPreference @JvmOverloads constructor(
init {
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) {

@ -31,12 +31,6 @@ class SecretSettingsFragment : PreferenceFragmentCompat() {
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 {
isVisible = FeatureFlags.waitUntilPaintToDraw
isChecked = context.settings().waitToShowPageUntilFirstPaint

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

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

@ -5,8 +5,12 @@
package org.mozilla.fenix.settings
import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceFragmentCompat
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.utils.view.addToRadioGroup
@ -23,6 +27,11 @@ class TabsSettingsFragment : PreferenceFragmentCompat() {
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() {
super.onResume()
showToolbar(getString(R.string.preferences_tabs))

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

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

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

@ -20,8 +20,9 @@ class LocaleViewHolder(
) : BaseLocaleViewHolder(view, selectedLocale) {
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)
// Show the given locale using the device locale for the subtitle.
locale_subtitle_text.text = locale.displayName.capitalize(Locale.getDefault())
locale_selected_icon.isVisible = isCurrentLocaleSelected(locale, isDefault = false)
@ -39,7 +40,8 @@ class SystemLocaleViewHolder(
override fun bind(locale: Locale) {
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)
itemView.setOnClickListener {
interactor.onDefaultLocaleSelected()

@ -141,36 +141,7 @@ class AddSearchEngineFragment : Fragment(R.layout.fragment_add_search_engine),
val name = edit_engine_name.text?.toString()?.trim() ?: ""
val searchString = edit_search_string.text?.toString() ?: ""
var hasError = false
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
}
val hasError = checkForErrors(name, searchString)
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)
}
SearchStringValidator.Result.Success -> {
CustomSearchEngineStore.addSearchEngine(
context = requireContext(),
engineName = name,
searchQuery = searchString
)
try {
CustomSearchEngineStore.addSearchEngine(
context = requireContext(),
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()
val successMessage = resources
.getString(R.string.search_add_custom_engine_success_message, name)
view?.also {
FenixSnackbar.make(view = it,
FenixSnackbar.make(
view = it,
duration = FenixSnackbar.LENGTH_SHORT,
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) {
viewLifecycleOwner.lifecycleScope.launch(Main) {
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 CUSTOM_INDEX = -1
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) {
private val args by navArgs<EditCustomSearchEngineFragmentArgs>()
private lateinit var searchEngine: SearchEngine
override fun onCreate(savedInstanceState: Bundle?) {
@ -85,6 +84,7 @@ class EditCustomSearchEngineFragment : Fragment(R.layout.fragment_add_search_eng
}
}
@Suppress("LongMethod")
private fun saveCustomEngine() {
custom_search_engine_name_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 searchString = edit_search_string.text?.toString() ?: ""
var hasError = false
if (name.isEmpty()) {
custom_search_engine_name_field.error = resources
.getString(R.string.search_add_custom_engine_error_empty_name)
hasError = true
}
val hasError = checkForErrors(name, searchString)
val existingIdentifiers = requireComponents
.search
.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
}
if (hasError) { return }
viewLifecycleOwner.lifecycleScope.launch(Main) {
lifecycleScope.launch(Main) {
val result = withContext(IO) {
SearchStringValidator.isSearchStringValid(
requireComponents.core.client,
@ -160,14 +131,50 @@ class EditCustomSearchEngineFragment : Fragment(R.layout.fragment_add_search_eng
.setText(successMessage)
.show()
}
if (args.isDefaultSearchEngine) {
requireComponents.search.provider.setDefaultEngine(requireContext(), name)
}
findNavController().popBackStack()
}
}
}
}
companion object {
private const val DPS_TO_INCREASE = 20
private fun checkForErrors(name: String, searchString: String): Boolean {
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) {
context.components.search.searchEngineManager.defaultSearchEngine = searchEngine
context.components.search.provider.setDefaultEngine(context, searchEngine.identifier)
context.settings().defaultSearchEngineName = searchEngine.name
}
}

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

@ -71,15 +71,14 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
return
}
val defaultEngine = context.components.search.provider.getDefaultEngine(context).identifier
val defaultEngineId = context.components.search.provider.getDefaultEngine(context).identifier
val selectedEngine = (searchEngineList.list.find {
it.identifier == defaultEngine
it.identifier == defaultEngineId
} ?: searchEngineList.list.first()).identifier
context.components.search.searchEngineManager.defaultSearchEngine =
searchEngineList.list.find {
it.identifier == selectedEngine
}
// set the search engine manager default
context.components.search.provider.setDefaultEngine(context, selectedEngine)
searchEngineGroup!!.removeAllViews()
@ -175,8 +174,9 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
}
private fun editCustomSearchEngine(engine: SearchEngine) {
val wasDefault = context.components.search.provider.getDefaultEngine(context).identifier == engine.identifier
val directions = SearchEngineFragmentDirections
.actionSearchEngineFragmentToEditCustomSearchEngineFragment(engine.identifier)
.actionSearchEngineFragmentToEditCustomSearchEngineFragment(engine.identifier, wasDefault)
Navigation.findNavController(searchEngineGroup!!).navigate(directions)
}
@ -215,12 +215,9 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
},
operation = {
if (isDefaultEngine) {
context.settings().defaultSearchEngineName = context
.components
.search
.provider
.getDefaultEngine(context)
.name
val default = context.components.search.provider.getDefaultEngine(context)
context.components.search.provider.setDefaultEngine(context, default.identifier)
context.settings().defaultSearchEngineName = default.name
}
if (isCustomSearchEngine) {
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.ext.getRootView
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.share.listadapters.AppShareOption
import org.mozilla.fenix.share.listadapters.SyncShareOption
class ShareFragment : AppCompatDialogFragment() {
@ -111,13 +113,13 @@ class ShareFragment : AppCompatDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.devicesList.observe(viewLifecycleOwner) { devicesShareOptions ->
viewModel.devicesList.observe<List<SyncShareOption>>(viewLifecycleOwner) { devicesShareOptions ->
shareToAccountDevicesView.setShareTargets(devicesShareOptions)
}
viewModel.appsList.observe(viewLifecycleOwner) { appsToShareTo ->
viewModel.appsList.observe<List<AppShareOption>>(viewLifecycleOwner) { appsToShareTo ->
shareToAppsView.setShareTargets(appsToShareTo)
}
viewModel.recentAppsList.observe(viewLifecycleOwner) { appsToShareTo ->
viewModel.recentAppsList.observe<List<AppShareOption>>(viewLifecycleOwner) { appsToShareTo ->
shareToAppsView.setRecentShareTargets(appsToShareTo)
}
}

@ -393,6 +393,8 @@ class TabTrayView(
} else {
setupRegularTabsTrayLayout()
}
// We will need to learn call setupGridTabView(), Mozilla's new
// official grid layout, by preference.
}
private fun setupCompactTabsTrayLayout() {
@ -465,8 +467,6 @@ class TabTrayView(
updateUINormalMode(view.context.components.core.store.state)
scrollToTab(view.context.components.core.store.state.selectedTabId)
view.tabsTray.invalidateItemDecorations()
if (isPrivateModeSelected) {
components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped)
} else {
@ -480,6 +480,26 @@ class TabTrayView(
var mode: Mode = Mode.Normal
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.
*/
@ -688,7 +708,7 @@ class TabTrayView(
view.tab_layout.getTabAt(0)?.contentDescription = if (count == 1) {
view.context?.getString(R.string.open_tab_tray_single)
} 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() {
@ -774,6 +794,10 @@ class TabTrayView(
}
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 =
appContext.getSharedPreferences(FENIX_PREFERENCES, MODE_PRIVATE)
var showTopFrecentSites by featureFlagPreference(
var showTopFrecentSites by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_enable_top_frecent_sites),
default = true,
featureFlag = FeatureFlags.topFrecentSite
default = true
)
var numberOfAppLaunches by intPreference(
@ -182,6 +181,11 @@ class Settings(private val appContext: Context) : PreferencesHolder {
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
fun shouldDisplayFenixMovingTip(): Boolean =
preferences.getBoolean(
@ -311,13 +315,16 @@ class Settings(private val appContext: Context) : PreferencesHolder {
val shouldShowSecurityPinWarning: Boolean
get() = loginsSecureWarningCount.underMaxCount()
fun shouldUseAutoSize() = fontSizeFactor == 1F
var shouldUseLightTheme by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_light_theme),
default = false
)
var shouldUseAutoSize by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_accessibility_auto_size),
default = true
)
var fontSizeFactor by floatPreference(
appContext.getPreferenceKey(R.string.pref_key_accessibility_font_scale),
default = 1f
@ -348,6 +355,16 @@ class Settings(private val appContext: Context) : PreferencesHolder {
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(
appContext.getPreferenceKey(R.string.pref_key_close_tabs_manually),
default = true
@ -375,6 +392,31 @@ class Settings(private val appContext: Context) : PreferencesHolder {
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 {
closeTabsAfterOneDay -> {
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()
}
val topSitesSize by intPreference(
var topSitesSize by intPreference(
appContext.getPreferenceKey(R.string.pref_key_top_sites_size),
default = 0
)
@ -924,18 +966,10 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = topSitesMaxCount
)
fun setOpenTabsCount(count: Int) {
preferences.edit().putInt(
appContext.getPreferenceKey(R.string.pref_key_open_tabs_count),
count
).apply()
}
val openTabsCount: Int
get() = preferences.getInt(
appContext.getPreferenceKey(R.string.pref_key_open_tabs_count),
0
)
var openTabsCount by intPreference(
appContext.getPreferenceKey(R.string.pref_key_open_tabs_count),
0
)
val customAddonsAccount by stringPreference(
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
android:id="@android:id/icon"
android:importantForAccessibility="no"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"

@ -20,9 +20,11 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<!-- MozMultipleConstraintLayouts: we're not changing the migration code. -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:ignore="MozMultipleConstraintLayouts">
<androidx.appcompat.widget.AppCompatImageView
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
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:id="@+id/linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/custom_amo_user"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/customize_addon_collection_user_hint"
android:singleLine="true"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
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
android:id="@+id/custom_amo_collection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/customize_addon_collection_hint"
android:singleLine="true"
android:layout_marginStart="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>

@ -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
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
@ -14,6 +15,7 @@
<Button
android:id="@+id/copy"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?selectableItemBackground"
@ -26,6 +28,7 @@
<Button
android:id="@+id/paste"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?selectableItemBackground"
@ -38,6 +41,7 @@
<Button
android:id="@+id/paste_and_go"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?selectableItemBackground"

@ -35,7 +35,7 @@
android:layout_height="48dp"
android:background="?selectableItemBackgroundBorderless"
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_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_close" />

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

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

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

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

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

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

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

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

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

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

@ -5,9 +5,11 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
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
tools:ignore="Instantiatable"
android:id="@+id/addonSettingsEngineView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

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

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

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

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

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

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

Loading…
Cancel
Save