Merge tag 'v88.0.0-beta.6' into upstream-sync

pull/420/head
Adam Novak 3 years ago
commit 39d40ebcc7

@ -3,7 +3,7 @@ on: [pull_request]
jobs:
run-build:
runs-on: ubuntu-20.04
if: github.event.pull_request.head.repo.full_name != github.repository
if: github.event.pull_request.head.repo.full_name != github.repository && github.actor != 'MickeyMoz'
steps:
- name: Checkout repository
uses: actions/checkout@v2
@ -21,7 +21,8 @@ jobs:
run-testDebugUnitTest:
runs-on: ubuntu-20.04
if: github.event.pull_request.head.repo.full_name != github.repository
if: github.event.pull_request.head.repo.full_name != github.repository && github.actor != 'MickeyMoz'
steps:
- name: Checkout repository
uses: actions/checkout@v2
@ -39,7 +40,8 @@ jobs:
run-detekt:
runs-on: ubuntu-20.04
if: github.event.pull_request.head.repo.full_name != github.repository
if: github.event.pull_request.head.repo.full_name != github.repository && github.actor != 'MickeyMoz'
steps:
- name: Checkout repository
uses: actions/checkout@v2
@ -62,7 +64,8 @@ jobs:
run-ktlint:
runs-on: ubuntu-20.04
if: github.event.pull_request.head.repo.full_name != github.repository
if: github.event.pull_request.head.repo.full_name != github.repository && github.actor != 'MickeyMoz'
steps:
- name: Checkout repository
uses: actions/checkout@v2
@ -80,7 +83,8 @@ jobs:
run-lintDebug:
runs-on: ubuntu-20.04
if: github.event.pull_request.head.repo.full_name != github.repository
if: github.event.pull_request.head.repo.full_name != github.repository && github.actor != 'MickeyMoz'
steps:
- name: Checkout repository
uses: actions/checkout@v2

@ -513,6 +513,7 @@ dependencies {
implementation Deps.mozilla_feature_accounts
implementation Deps.mozilla_feature_app_links
implementation Deps.mozilla_feature_autofill
implementation Deps.mozilla_feature_awesomebar
implementation Deps.mozilla_feature_contextmenu
implementation Deps.mozilla_feature_customtabs
@ -544,6 +545,7 @@ dependencies {
implementation Deps.mozilla_feature_webcompat_reporter
implementation Deps.mozilla_service_digitalassetlinks
implementation Deps.mozilla_service_sync_autofill
implementation Deps.mozilla_service_sync_logins
implementation Deps.mozilla_service_firefox_accounts
implementation Deps.mozilla_service_glean

File diff suppressed because it is too large Load Diff

@ -27,6 +27,7 @@ import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.GleanBuildInfo
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.HomeActivityTestRule
@ -85,9 +86,10 @@ class BaselinePingTest {
// we need to do this on the main thread, as the Glean SDK requires it.
GlobalScope.launch(Dispatchers.Main.immediate) {
Glean.initialize(
ApplicationProvider.getApplicationContext(),
true,
Configuration(httpClient = httpClient)
applicationContext = ApplicationProvider.getApplicationContext(),
uploadEnabled = true,
configuration = Configuration(httpClient = httpClient),
buildInfo = GleanBuildInfo.buildInfo
)
}
}

@ -9,6 +9,7 @@ import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.ui.robots.appContext
@ -37,6 +38,7 @@ class HomeActivityTestRule(
override fun afterActivityFinished() {
super.afterActivityFinished()
setLongTapTimeout(longTapUserPreference)
closeNotificationShade()
}
}
@ -65,6 +67,7 @@ class HomeActivityIntentTestRule(
override fun afterActivityFinished() {
super.afterActivityFinished()
setLongTapTimeout(longTapUserPreference)
closeNotificationShade()
}
}
@ -79,3 +82,13 @@ private fun skipOnboardingBeforeLaunch() {
// this API so it can be fragile.
FenixOnboarding(appContext).finish()
}
private fun closeNotificationShade() {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
if (mDevice.findObject(
UiSelector().resourceId("com.android.systemui:id/notification_stack_scroller")
).exists()
) {
mDevice.pressHome()
}
}

@ -11,13 +11,16 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.preference.PreferenceManager
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
@ -25,7 +28,9 @@ import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.idlingresource.NetworkConnectionIdlingResource
import org.mozilla.fenix.ui.robots.mDevice
import java.io.File
@ -84,6 +89,7 @@ object TestHelper {
fun verifyUrl(urlSubstring: String, resourceName: String, resId: Int) {
waitUntilObjectIsFound(resourceName)
mDevice.findObject(UiSelector().text(urlSubstring)).waitForExists(waitingTime)
onView(withId(resId)).check(ViewAssertions.matches(withText(CoreMatchers.containsString(urlSubstring))))
}
@ -117,4 +123,34 @@ object TestHelper {
}
}
}
fun setNetworkEnabled(enabled: Boolean) {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
val networkDisconnectedIdlingResource = NetworkConnectionIdlingResource(false)
val networkConnectedIdlingResource = NetworkConnectionIdlingResource(true)
when (enabled) {
true -> {
mDevice.executeShellCommand("svc data enable")
mDevice.executeShellCommand("svc wifi enable")
// Wait for network connection to be completely enabled
IdlingRegistry.getInstance().register(networkConnectedIdlingResource)
Espresso.onIdle {
IdlingRegistry.getInstance().unregister(networkConnectedIdlingResource)
}
}
false -> {
mDevice.executeShellCommand("svc data disable")
mDevice.executeShellCommand("svc wifi disable")
// Wait for network connection to be completely disabled
IdlingRegistry.getInstance().register(networkDisconnectedIdlingResource)
Espresso.onIdle {
IdlingRegistry.getInstance().unregister(networkDisconnectedIdlingResource)
}
}
}
}
}

@ -0,0 +1,48 @@
/* 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.helpers.idlingresource
import android.net.ConnectivityManager
import androidx.core.content.getSystemService
import androidx.test.espresso.IdlingResource
import androidx.test.platform.app.InstrumentationRegistry
import org.mozilla.fenix.ext.isOnline
/**
* An IdlingResource implementation that waits until the network connection is online or offline.
* The networkConnected parameter sets the expected connection status.
* Only after connecting/disconnecting has completed further actions will be performed.
*/
class NetworkConnectionIdlingResource(private val networkConnected: Boolean) : IdlingResource {
private var resourceCallback: IdlingResource.ResourceCallback? = null
private val connectionManager =
InstrumentationRegistry.getInstrumentation().context.getSystemService<ConnectivityManager>()
override fun getName(): String {
return this::javaClass.name
}
override fun isIdleNow(): Boolean {
val idle =
if (networkConnected) {
isOnline()
} else {
!isOnline()
}
if (idle)
resourceCallback?.onTransitionToIdle()
return idle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
if (callback != null)
resourceCallback = callback
}
private fun isOnline(): Boolean {
return connectionManager!!.isOnline()
}
}

@ -18,17 +18,25 @@
"default": {
"atomicwrites": {
"hashes": [
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
"sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197",
"sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"
],
"version": "==1.3.0"
"version": "==1.4.0"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
"version": "==20.3.0"
},
"backports.functools-lru-cache": {
"hashes": [
"sha256:0bada4c2f8a43d533e4ecb7a12214d9420e66eb206d54bf2d682581ca4b80848",
"sha256:8fde5f188da2d593bd5bc0be98d9abc46c95bb8a9dde93429570192ee6cc2d4a"
],
"version": "==19.3.0"
"markers": "python_version < '3.2'",
"version": "==1.6.1"
},
"blessings": {
"hashes": [
@ -40,43 +48,51 @@
},
"certifi": {
"hashes": [
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
],
"version": "==2019.11.28"
"version": "==2020.12.5"
},
"cffi": {
"hashes": [
"sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
"sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
"sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
"sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
"sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
"sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
"sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
"sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
"sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
"sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
"sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
"sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
"sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
"sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
"sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
"sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
"sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
"sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
"sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
"sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
"sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
"sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
"sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
"sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
"sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
"sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
"sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
"sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
],
"version": "==1.14.0"
"sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e",
"sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d",
"sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a",
"sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec",
"sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362",
"sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668",
"sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c",
"sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b",
"sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06",
"sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698",
"sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2",
"sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c",
"sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7",
"sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009",
"sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03",
"sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b",
"sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909",
"sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53",
"sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35",
"sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26",
"sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b",
"sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01",
"sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb",
"sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293",
"sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd",
"sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d",
"sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3",
"sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d",
"sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e",
"sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca",
"sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d",
"sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775",
"sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375",
"sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b",
"sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b",
"sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"
],
"version": "==1.14.4"
},
"chardet": {
"hashes": [
@ -103,36 +119,38 @@
},
"cryptography": {
"hashes": [
"sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c",
"sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595",
"sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad",
"sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651",
"sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2",
"sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff",
"sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d",
"sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42",
"sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d",
"sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e",
"sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912",
"sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793",
"sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13",
"sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7",
"sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0",
"sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879",
"sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f",
"sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9",
"sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2",
"sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf",
"sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"
],
"version": "==2.8"
"sha256:22f8251f68953553af4f9c11ec5f191198bc96cff9f0ac5dd5ff94daede0ee6d",
"sha256:284e275e3c099a80831f9898fb5c9559120d27675c3521278faba54e584a7832",
"sha256:3e17d02941c0f169c5b877597ca8be895fca0e5e3eb882526a74aa4804380a98",
"sha256:52a47e60953679eea0b4d490ca3c241fb1b166a7b161847ef4667dfd49e7699d",
"sha256:57b8c1ed13b8aa386cabbfde3be175d7b155682470b0e259fecfe53850967f8a",
"sha256:6a8f64ed096d13f92d1f601a92d9fd1f1025dc73a2ca1ced46dcf5e0d4930943",
"sha256:6e8a3c7c45101a7eeee93102500e1b08f2307c717ff553fcb3c1127efc9b6917",
"sha256:7ef41304bf978f33cfb6f43ca13bb0faac0c99cda33693aa20ad4f5e34e8cb8f",
"sha256:87c2fffd61e934bc0e2c927c3764c20b22d7f5f7f812ee1a477de4c89b044ca6",
"sha256:88069392cd9a1e68d2cfd5c3a2b0d72a44ef3b24b8977a4f7956e9e3c4c9477a",
"sha256:8a0866891326d3badb17c5fd3e02c926b635e8923fa271b4813cd4d972a57ff3",
"sha256:8f0fd8b0751d75c4483c534b209e39e918f0d14232c0d8a2a76e687f64ced831",
"sha256:9a07e6d255053674506091d63ab4270a119e9fc83462c7ab1dbcb495b76307af",
"sha256:9a8580c9afcdcddabbd064c0a74f337af74ff4529cdf3a12fa2e9782d677a2e5",
"sha256:bd80bc156d3729b38cb227a5a76532aef693b7ac9e395eea8063ee50ceed46a5",
"sha256:d1cbc3426e6150583b22b517ef3720036d7e3152d428c864ff0f3fcad2b97591",
"sha256:e15ac84dcdb89f92424cbaca4b0b34e211e7ce3ee7b0ec0e4f3c55cee65fae5a",
"sha256:e4789b84f8dedf190148441f7c5bfe7244782d9cbb194a36e17b91e7d3e1cca9",
"sha256:f01c9116bfb3ad2831e125a73dcd957d173d6ddca7701528eff1e7d97972872c",
"sha256:f0e3986f6cce007216b23c490f093f35ce2068f3c244051e559f647f6731b7ae",
"sha256:f2aa3f8ba9e2e3fd49bd3de743b976ab192fbf0eb0348cebde5d2a9de0090a9f",
"sha256:fb70a4cedd69dc52396ee114416a3656e011fb0311fca55eb55c7be6ed9c8aef"
],
"index": "pypi",
"version": "==3.2"
},
"distro": {
"hashes": [
"sha256:362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57",
"sha256:eedf82a470ebe7d010f1872c17237c79ab04097948800029994fa458e52fb4b4"
"sha256:0e58756ae38fbd8fc3020d54badb8eae17c5b9dcbed388b17bb55b8a5928df92",
"sha256:df74eed763e18d10d0da624258524ae80486432cd17392d9c3d96f5e83cd2799"
],
"version": "==1.4.0"
"version": "==1.5.0"
},
"enum34": {
"hashes": [
@ -168,18 +186,18 @@
},
"idna": {
"hashes": [
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.9"
"version": "==2.10"
},
"importlib-metadata": {
"hashes": [
"sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302",
"sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"
"sha256:b8de9eff2b35fb037368f28a7df1df4e6436f578fa74423505b6c6a778d5b5dd",
"sha256:c2d6341ff566f609e89a2acb2db190e5e1d23d5409d6cc8d2fe34d72443876d4"
],
"markers": "python_version < '3.8'",
"version": "==1.5.0"
"version": "==2.1.1"
},
"ipaddress": {
"hashes": [
@ -200,10 +218,10 @@
},
"mozdevice": {
"hashes": [
"sha256:3f19640478fae47c5dfed998da42f86aef39d9ba03222e99d19ff488e2f78184",
"sha256:e6f7a679ef9a40e6db8e1d47d611b8d9997f03d345953570cbdf34d7cde79b6e"
"sha256:074ba1ff99b18ccc1931538a161be2410d0f9cee122df852b3bc73e1000fbcad",
"sha256:a5a1e882a72df71165f6322def9b5e1d677d39d25f62157f3e0dc554b5ae04dc"
],
"version": "==3.1.0"
"version": "==4.0.3"
},
"mozdownload": {
"hashes": [
@ -221,30 +239,31 @@
},
"mozinfo": {
"hashes": [
"sha256:41220573ea1e0e90d38d393f2848b470aeea68c1863eef0d578eb44c70a2f817",
"sha256:65e158464e09ba759f21526c14bef2f8c0ae5b424f14f23e7d863048e16a402a"
"sha256:4961ebef3c5474b9ca470142f88b5de774a069f4e105663a5152b0ef4659785a"
],
"version": "==1.2.1"
"version": "==1.2.2"
},
"mozinstall": {
"hashes": [
"sha256:219ba7c51308433487b4f30a2615cb9b3ecd40a76b9faf41cf1b1b005bb5dda7"
"sha256:219ba7c51308433487b4f30a2615cb9b3ecd40a76b9faf41cf1b1b005bb5dda7",
"sha256:bbc31a18ee8a1fbec74b67b99c6c0289ffc7daf39eb5b5ff7dc99f1be687eb08"
],
"index": "pypi",
"version": "==2.0.0"
},
"mozlog": {
"hashes": [
"sha256:4eb8b781525a1433f05239583051dfe19927036b0c1f450ef0ad33882284a13b",
"sha256:50608bd9f58461dc266be0e3663b6f97934b02a12bb403f2ccf09728e36f4898"
"sha256:4719d3d00bf1a0b77285d306eb3180f9c1311fffae9640a423fad9d80170e43d",
"sha256:d035f722c15d700e4a7b48b90bdda0a6ad83e25482760949d1abd73468bad07f"
],
"version": "==6.0"
"version": "==7.0.1"
},
"mozprocess": {
"hashes": [
"sha256:949fe2c96c866b3bd89011441b85dbe0477d1a2d3dde5030a35cf51f28db793d"
"sha256:08e1036b53819fd144331f6dfbbb17fc8ca782bbed2e28b4aa771b8b91f7dffb",
"sha256:54dc59e7f5a9c2c2930bffb7935f36dddd1d94c9fc6ed179e893d2dff353995a"
],
"version": "==1.1.0"
"version": "==1.2.1"
},
"mozprofile": {
"hashes": [
@ -279,17 +298,17 @@
},
"packaging": {
"hashes": [
"sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3",
"sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"
"sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236",
"sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"
],
"version": "==20.3"
"version": "==20.7"
},
"pathlib2": {
"hashes": [
"sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db",
"sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"
],
"markers": "python_version < '3'",
"markers": "python_version < '3.6'",
"version": "==2.3.5"
},
"pluggy": {
@ -301,17 +320,17 @@
},
"progressbar2": {
"hashes": [
"sha256:489cc619276505504cb3e6230e05465480f7b748515a2c1374f82115bcae561c",
"sha256:e5dcded3957b40eab6ba23962d25618a7709971b5413078a5905a93e84345e92"
"sha256:ef72be284e7f2b61ac0894b44165926f13f5d995b2bf3cd8a8dedc6224b255a7",
"sha256:fe2738e7ecb7df52ad76307fe610c460c52b50f5335fd26c3ab80ff7655ba1e0"
],
"version": "==3.50.0"
"version": "==3.53.1"
},
"py": {
"hashes": [
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
],
"version": "==1.8.1"
"version": "==1.9.0"
},
"pybrowserid": {
"hashes": [
@ -329,16 +348,23 @@
},
"pyfxa": {
"hashes": [
"sha256:f47f4285629fa6c033c79adc3fb90926c0818a42cfddb04d32818547362f1627"
"sha256:6c85cd08cf05f7138dee1cf2a8a1d68fd428b7b5ad488917c70a2a763d651cdb"
],
"version": "==0.7.7"
},
"pyjwt": {
"hashes": [
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
"sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
],
"version": "==0.7.3"
"version": "==1.7.1"
},
"pyparsing": {
"hashes": [
"sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
"sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"version": "==2.4.6"
"version": "==2.4.7"
},
"pypom": {
"hashes": [
@ -427,10 +453,10 @@
},
"six": {
"hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"version": "==1.14.0"
"version": "==1.15.0"
},
"treeherder-client": {
"hashes": [
@ -441,17 +467,17 @@
},
"urllib3": {
"hashes": [
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
"sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
"sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
],
"version": "==1.25.8"
"version": "==1.25.11"
},
"wcwidth": {
"hashes": [
"sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603",
"sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
"sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
],
"version": "==0.1.8"
"version": "==0.2.5"
},
"webob": {
"hashes": [
@ -469,9 +495,10 @@
},
"zope.component": {
"hashes": [
"sha256:ec2afc5bbe611dcace98bb39822c122d44743d635dafc7315b9aef25097db9e6"
"sha256:607628e4c84f7887a69a958542b5c304663e726b73aba0882e3a3f059bff14f3",
"sha256:91628918218b3e6f6323de2a7b845e09ddc5cae131c034896c051b084bba3c92"
],
"version": "==4.6"
"version": "==4.6.2"
},
"zope.deferredimport": {
"hashes": [
@ -489,10 +516,10 @@
},
"zope.event": {
"hashes": [
"sha256:69c27debad9bdacd9ce9b735dad382142281ac770c4a432b533d6d65c4614bcf",
"sha256:d8e97d165fd5a0997b45f5303ae11ea3338becfe68c401dd88ffd2113fe5cae7"
"sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42",
"sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330"
],
"version": "==4.4"
"version": "==4.5.0"
},
"zope.hookable": {
"hashes": [
@ -541,91 +568,105 @@
},
"zope.interface": {
"hashes": [
"sha256:02339c53bbf7e438dec371af1f401e4843f9dc5765b3b032032b195dd72b47f2",
"sha256:0616040d5a18786aff5d25ef6e1fa0f875b7ba5b6f1a923c1153be81dd9c65ad",
"sha256:07a8bb9110854c0ab9329adbbec7050af242a78a62e226ab49e9c2182090f501",
"sha256:07de051fac6dedc6c659034f80bc46623edc776c757fa26f3f467954b12d2403",
"sha256:08ae0a88ac29b92faff069e0511ad27197b3274bdf67ebd8c75aaeb05823c7af",
"sha256:1033e7bb858c398580ca7cbb50f15b715e6031d5772f8a1bde4042c12300a52a",
"sha256:11db683f49652b34aa87904b27d00f9032fa2db7f1f9676c05b13361a3c7547c",
"sha256:23c4a70a9abb8faa35e2967e2e7cbd9225512b706b6eb96b01eb1ccbb2b632c3",
"sha256:2690fd5b042062d866017db11ce1e12d4862df28614cc2915dc57e52b46a8594",
"sha256:31fdcc9eaf2c379e8b416184a0749ce3f95fdaf206b092b63bdc065aecca6a95",
"sha256:34381fcecc6e6f57d72bc2fab6175976eeacdd61dbb34427a37b260238278199",
"sha256:36e7438d2f71153cea6b119ddd2648bc010cec4368fd8e7170e50090c0d7ed19",
"sha256:3b6a2ef2c6b4e9786939bd9861e7b98bc01cb3024f87c8cf4b78872f2afcf584",
"sha256:4855384c27fe7e31efbee32f74659421d64e5bfa8b7df2ec08d306d0f3d4cee6",
"sha256:4d0830e1d544b2c303064ec01923de2b9d6f5b5d0d78608a91d758b0f469361c",
"sha256:61b95dbfd03ce2a55c38da711cba7130605dbef4839ca12b53c46827826c5c5b",
"sha256:64446f9baa2c51f47b0e272939c583ffd220e67f5bcbc2f18dd244c5a46a7018",
"sha256:65cef4766be4be9372621cd17773424302c21785dfaf6e9bd5b64b1f1264f9cc",
"sha256:6f1e8914eee2e3a0bcf435d963ca5cf3a3df89a47cbd3e2b16343bc875194fed",
"sha256:6f2bf246ee9350f428860c37db6158cfb27a7e585d60b2bb3b88864810875835",
"sha256:73a618e734803ded8b8d8f14f9a6371c6a1acc445840cf6ae57733041e796671",
"sha256:7d3c4f10b7a8502a68a8eadcd57e86a35e3948af3edee7bd49a21b225361b0da",
"sha256:98a21acc7d1e45fcb700831b66ec6c84a3c2a5a94868d72ef83565966adc474f",
"sha256:993051db4278f9ec3b191ae823a7bb32b6a91fed6e960d43500fc4ce64cdb4e0",
"sha256:9e67e9fa7dc43210ad766cd6eef133d9d256a530fe07e5585685cdc742170d10",
"sha256:a36e7e1972109504dfa0995a89b6c24a990113eb4cedef93d0eaf1452901b6ac",
"sha256:a41a34c55887743ee124e8f696217dec1a7eead1164d27ef27dfae528c396a23",
"sha256:a7b50fa86c1bd863ef3b3314da62928c015a732bb0aef220852b9606104f0df5",
"sha256:a82d36ecc28e72904388f72f57f3c04aee7c43a019e302d61944b3886c261be3",
"sha256:a9fce290a6ba88e5e6e81dd1e800c045212df69ab69d1de0d303b1af9cec692f",
"sha256:ae6c4a1fa696c12c3b654fa0d160f3781271f0edbbb0ae50f285a91f2a272a09",
"sha256:b0029f193d91a1e0085e4a99dd71e4c63a3e7826ec4a8d2ea457f02e1b6b0bb4",
"sha256:b12241fac561b635329c3275e911a53e104b43875b99d31332d006d52e180912",
"sha256:b906dda76ab70b6905ef3014260e7f1c861a0c4841e29826eb34a6187255504b",
"sha256:d79da12a15edd6d7c366766954c4b6de0247e85ba35ee2ad9f37f972e7080f8a",
"sha256:d8a0cb84de725ccd6abd9b5bd32cb94a11db336076fb6d459f1fed23d0719e0c",
"sha256:eb92c733be08c6e2b8dfd4613d1b3c2f345ca4f83219d40fda4680333d3a0dc4",
"sha256:f044fec9c7e1b0ec6fdf0d3abc648c2f3b9128933051a9a73af52dbdd9e6d6e9",
"sha256:fd1101bd3fcb4f4cf3485bb20d6cb0b56909b94d3bd2a53a6cb9d381c3da3365"
],
"version": "==4.7.2"
"sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1",
"sha256:07d61722dd7d85547b7c6b0f5486b4338001fab349f2ac5cabc0b7182eb3425d",
"sha256:0a990dcc97806e5980bbb54b2e46b9cde9e48932d8e6984daf71ef1745516123",
"sha256:150e8bcb7253a34a4535aeea3de36c0bb3b1a6a47a183a95d65a194b3e07f232",
"sha256:1743bcfe45af8846b775086471c28258f4c6e9ee8ef37484de4495f15a98b549",
"sha256:1b5f6c8fff4ed32aa2dd43e84061bc8346f32d3ba6ad6e58f088fe109608f102",
"sha256:21e49123f375703cf824214939d39df0af62c47d122d955b2a8d9153ea08cfd5",
"sha256:21f579134a47083ffb5ddd1307f0405c91aa8b61ad4be6fd5af0171474fe0c45",
"sha256:27c267dc38a0f0079e96a2945ee65786d38ef111e413c702fbaaacbab6361d00",
"sha256:299bde0ab9e5c4a92f01a152b7fbabb460f31343f1416f9b7b983167ab1e33bc",
"sha256:2ab88d8f228f803fcb8cb7d222c579d13dab2d3622c51e8cf321280da01102a7",
"sha256:2ced4c35061eea623bc84c7711eedce8ecc3c2c51cd9c6afa6290df3bae9e104",
"sha256:2dcab01c660983ba5e5a612e0c935141ccbee67d2e2e14b833e01c2354bd8034",
"sha256:32546af61a9a9b141ca38d971aa6eb9800450fa6620ce6323cc30eec447861f3",
"sha256:32b40a4c46d199827d79c86bb8cb88b1bbb764f127876f2cb6f3a47f63dbada3",
"sha256:3cc94c69f6bd48ed86e8e24f358cb75095c8129827df1298518ab860115269a4",
"sha256:42b278ac0989d6f5cf58d7e0828ea6b5951464e3cf2ff229dd09a96cb6ba0c86",
"sha256:495b63fd0302f282ee6c1e6ea0f1c12cb3d1a49c8292d27287f01845ff252a96",
"sha256:4af87cdc0d4b14e600e6d3d09793dce3b7171348a094ba818e2a68ae7ee67546",
"sha256:4b94df9f2fdde7b9314321bab8448e6ad5a23b80542dcab53e329527d4099dcb",
"sha256:4c48ddb63e2b20fba4c6a2bf81b4d49e99b6d4587fb67a6cd33a2c1f003af3e3",
"sha256:4df9afd17bd5477e9f8c8b6bb8507e18dd0f8b4efe73bb99729ff203279e9e3b",
"sha256:518950fe6a5d56f94ba125107895f938a4f34f704c658986eae8255edb41163b",
"sha256:538298e4e113ccb8b41658d5a4b605bebe75e46a30ceca22a5a289cf02c80bec",
"sha256:55465121e72e208a7b69b53de791402affe6165083b2ea71b892728bd19ba9ae",
"sha256:588384d70a0f19b47409cfdb10e0c27c20e4293b74fc891df3d8eb47782b8b3e",
"sha256:6278c080d4afffc9016e14325f8734456831124e8c12caa754fd544435c08386",
"sha256:64ea6c221aeee4796860405e1aedec63424cda4202a7ad27a5066876db5b0fd2",
"sha256:681dbb33e2b40262b33fd383bae63c36d33fd79fa1a8e4092945430744ffd34a",
"sha256:6936aa9da390402d646a32a6a38d5409c2d2afb2950f045a7d02ab25a4e7d08d",
"sha256:778d0ec38bbd288b150a3ae363c8ffd88d2207a756842495e9bffd8a8afbc89a",
"sha256:8251f06a77985a2729a8bdbefbae79ee78567dddc3acbd499b87e705ca59fe24",
"sha256:83b4aa5344cce005a9cff5d0321b2e318e871cc1dfc793b66c32dd4f59e9770d",
"sha256:844fad925ac5c2ad4faaceb3b2520ad016b5280105c6e16e79838cf951903a7b",
"sha256:8ceb3667dd13b8133f2e4d637b5b00f240f066448e2aa89a41f4c2d78a26ce50",
"sha256:92dc0fb79675882d0b6138be4bf0cec7ea7c7eede60aaca78303d8e8dbdaa523",
"sha256:9789bd945e9f5bd026ed3f5b453d640befb8b1fc33a779c1fe8d3eb21fe3fb4a",
"sha256:a2b6d6eb693bc2fc6c484f2e5d93bd0b0da803fa77bf974f160533e555e4d095",
"sha256:aab9f1e34d810feb00bf841993552b8fcc6ae71d473c505381627143d0018a6a",
"sha256:abb61afd84f23099ac6099d804cdba9bd3b902aaaded3ffff47e490b0a495520",
"sha256:adf9ee115ae8ff8b6da4b854b4152f253b390ba64407a22d75456fe07dcbda65",
"sha256:aedc6c672b351afe6dfe17ff83ee5e7eb6ed44718f879a9328a68bdb20b57e11",
"sha256:b7a00ecb1434f8183395fac5366a21ee73d14900082ca37cf74993cf46baa56c",
"sha256:ba32f4a91c1cb7314c429b03afbf87b1fff4fb1c8db32260e7310104bd77f0c7",
"sha256:cbd0f2cbd8689861209cd89141371d3a22a11613304d1f0736492590aa0ab332",
"sha256:e4bc372b953bf6cec65a8d48482ba574f6e051621d157cf224227dbb55486b1e",
"sha256:eccac3d9aadc68e994b6d228cb0c8919fc47a5350d85a1b4d3d81d1e98baf40c",
"sha256:efd550b3da28195746bb43bd1d815058181a7ca6d9d6aa89dd37f5eefe2cacb7",
"sha256:efef581c8ba4d990770875e1a2218e856849d32ada2680e53aebc5d154a17e20",
"sha256:f057897711a630a0b7a6a03f1acf379b6ba25d37dc5dc217a97191984ba7f2fc",
"sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd",
"sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537"
],
"version": "==5.2.0"
},
"zope.proxy": {
"hashes": [
"sha256:04646ac04ffa9c8e32fb2b5c3cd42995b2548ea14251f3c21ca704afae88e42c",
"sha256:07b6bceea232559d24358832f1cd2ed344bbf05ca83855a5b9698b5f23c5ed60",
"sha256:1ef452cc02e0e2f8e3c917b1a5b936ef3280f2c2ca854ee70ac2164d1655f7e6",
"sha256:22bf61857c5977f34d4e391476d40f9a3b8c6ab24fb0cac448d42d8f8b9bf7b2",
"sha256:299870e3428cbff1cd9f9b34144e76ecdc1d9e3192a8cf5f1b0258f47a239f58",
"sha256:2bfc36bfccbe047671170ea5677efd3d5ab730a55d7e45611d76d495e5b96766",
"sha256:32e82d5a640febc688c0789e15ea875bf696a10cf358f049e1ed841f01710a9b",
"sha256:3b2051bdc4bc3f02fa52483f6381cf40d4d48167645241993f9d7ebbd142ed9b",
"sha256:3f734bd8a08f5185a64fb6abb8f14dc97ec27a689ca808fb7a83cdd38d745e4f",
"sha256:3f78dd8de3112df8bbd970f0916ac876dc3fbe63810bd1cf7cc5eec4cbac4f04",
"sha256:4eabeb48508953ba1f3590ad0773b8daea9e104eec66d661917e9bbcd7125a67",
"sha256:4f05ecc33808187f430f249cb1ccab35c38f570b181f2d380fbe253da94b18d8",
"sha256:4f4f4cbf23d3afc1526294a31e7b3eaa0f682cc28ac5366065dc1d6bb18bd7be",
"sha256:5483d5e70aacd06f0aa3effec9fed597c0b50f45060956eeeb1203c44d4338c3",
"sha256:56a5f9b46892b115a75d0a1f2292431ad5988461175826600acc69a24cb3edee",
"sha256:64bb63af8a06f736927d260efdd4dfc5253d42244f281a8063e4b9eea2ddcbc5",
"sha256:653f8cbefcf7c6ac4cece2cdef367c4faa2b7c19795d52bd7cbec11a8739a7c1",
"sha256:664211d63306e4bd4eec35bf2b4bd9db61c394037911cf2d1804c43b511a49f1",
"sha256:6651e6caed66a8fff0fef1a3e81c0ed2253bf361c0fdc834500488732c5d16e9",
"sha256:6c1fba6cdfdf105739d3069cf7b07664f2944d82a8098218ab2300a82d8f40fc",
"sha256:6e64246e6e9044a4534a69dca1283c6ddab6e757be5e6874f69024329b3aa61f",
"sha256:838390245c7ec137af4993c0c8052f49d5ec79e422b4451bfa37fee9b9ccaa01",
"sha256:856b410a14793069d8ba35f33fff667213ea66f2df25a0024cc72a7493c56d4c",
"sha256:8b932c364c1d1605a91907a41128ed0ee8a2d326fc0fafb2c55cd46f545f4599",
"sha256:9086cf6d20f08dae7f296a78f6c77d1f8d24079d448f023ee0eb329078dd35e1",
"sha256:9698533c14afa0548188de4968a7932d1f3f965f3f5ba1474de673596bb875af",
"sha256:9b12b05dd7c28f5068387c1afee8cb94f9d02501e7ef495a7c5c7e27139b96ad",
"sha256:a884c7426a5bc6fb7fc71a55ad14e66818e13f05b78b20a6f37175f324b7acb8",
"sha256:abe9e7f1a3e76286c5f5baf2bf5162d41dc0310da493b34a2c36555f38d928f7",
"sha256:bd6fde63b015a27262be06bd6bbdd895273cc2bdf2d4c7e1c83711d26a8fbace",
"sha256:bda7c62c954f47b87ed9a89f525eee1b318ec7c2162dfdba76c2ccfa334e0caa",
"sha256:be8a4908dd3f6e965993c0068b006bdbd0474fbcbd1da4893b49356e73fc1557",
"sha256:ced65fc3c7d7205267506d854bb1815bb445899cca9d21d1d4b949070a635546",
"sha256:dac4279aa05055d3897ab5e5ee5a7b39db121f91df65a530f8b1ac7f9bd93119",
"sha256:e4f1863056e3e4f399c285b67fa816f411a7bfa1c81ef50e186126164e396e59",
"sha256:ecd85f68b8cd9ab78a0141e87ea9a53b2f31fd9b1350a1c44da1f7481b5363ef",
"sha256:ed269b83750413e8fc5c96276372f49ee3fcb7ed61c49fe8e5a67f54459a5a4a",
"sha256:f19b0b80cba73b204dee68501870b11067711d21d243fb6774256d3ca2e5391f",
"sha256:ffdafb98db7574f9da84c489a10a5d582079a888cb43c64e9e6b0e3fe1034685"
],
"version": "==4.3.3"
"sha256:00573dfa755d0703ab84bb23cb6ecf97bb683c34b340d4df76651f97b0bab068",
"sha256:092049280f2848d2ba1b57b71fe04881762a220a97b65288bcb0968bb199ec30",
"sha256:0cbd27b4d3718b5ec74fc65ffa53c78d34c65c6fd9411b8352d2a4f855220cf1",
"sha256:17fc7e16d0c81f833a138818a30f366696653d521febc8e892858041c4d88785",
"sha256:19577dfeb70e8a67249ba92c8ad20589a1a2d86a8d693647fa8385408a4c17b0",
"sha256:207aa914576b1181597a1516e1b90599dc690c095343ae281b0772e44945e6a4",
"sha256:219a7db5ed53e523eb4a4769f13105118b6d5b04ed169a283c9775af221e231f",
"sha256:2b50ea79849e46b5f4f2b0247a3687505d32d161eeb16a75f6f7e6cd81936e43",
"sha256:5903d38362b6c716e66bbe470f190579c530a5baf03dbc8500e5c2357aa569a5",
"sha256:5c24903675e271bd688c6e9e7df5775ac6b168feb87dbe0e4bcc90805f21b28f",
"sha256:5ef6bc5ed98139e084f4e91100f2b098a0cd3493d4e76f9d6b3f7b95d7ad0f06",
"sha256:61b55ae3c23a126a788b33ffb18f37d6668e79a05e756588d9e4d4be7246ab1c",
"sha256:63ddb992931a5e616c87d3d89f5a58db086e617548005c7f9059fac68c03a5cc",
"sha256:6943da9c09870490dcfd50c4909c0cc19f434fa6948f61282dc9cb07bcf08160",
"sha256:6ad40f85c1207803d581d5d75e9ea25327cd524925699a83dfc03bf8e4ba72b7",
"sha256:6b44433a79bdd7af0e3337bd7bbcf53dd1f9b0fa66bf21bcb756060ce32a96c1",
"sha256:6bbaa245015d933a4172395baad7874373f162955d73612f0b66b6c2c33b6366",
"sha256:7007227f4ea85b40a2f5e5a244479f6a6dfcf906db9b55e812a814a8f0e2c28d",
"sha256:74884a0aec1f1609190ec8b34b5d58fb3b5353cf22b96161e13e0e835f13518f",
"sha256:7d25fe5571ddb16369054f54cdd883f23de9941476d97f2b92eb6d7d83afe22d",
"sha256:7e162bdc5e3baad26b2262240be7d2bab36991d85a6a556e48b9dfb402370261",
"sha256:814d62678dc3a30f4aa081982d830b7c342cf230ffc9d030b020cb154eeebf9e",
"sha256:8878a34c5313ee52e20aa50b03138af8d472bae465710fb954d133a9bfd3c38d",
"sha256:a66a0d94e5b081d5d695e66d6667e91e74d79e273eee95c1747717ba9cb70792",
"sha256:a69f5cbf4addcfdf03dda564a671040127a6b7c34cf9fe4973582e68441b63fa",
"sha256:b00f9f0c334d07709d3f73a7cb8ae63c6ca1a90c790a63b5e7effa666ef96021",
"sha256:b6ed71e4a7b4690447b626f499d978aa13197a0e592950e5d7020308f6054698",
"sha256:bdf5041e5851526e885af579d2f455348dba68d74f14a32781933569a327fddf",
"sha256:be034360dd34e62608419f86e799c97d389c10a0e677a25f236a971b2f40dac9",
"sha256:cc8f590a5eed30b314ae6b0232d925519ade433f663de79cc3783e4b10d662ba",
"sha256:cd7a318a15fe6cc4584bf3c4426f092ed08c0fd012cf2a9173114234fe193e11",
"sha256:cf19b5f63a59c20306e034e691402b02055c8f4e38bf6792c23cad489162a642",
"sha256:cfc781ce442ec407c841e9aa51d0e1024f72b6ec34caa8fdb6ef9576d549acf2",
"sha256:dea9f6f8633571e18bc20cad83603072e697103a567f4b0738d52dd0211b4527",
"sha256:e4a86a1d5eb2cce83c5972b3930c7c1eac81ab3508464345e2b8e54f119d5505",
"sha256:e7106374d4a74ed9ff00c46cc00f0a9f06a0775f8868e423f85d4464d2333679",
"sha256:e98a8a585b5668aa9e34d10f7785abf9545fe72663b4bfc16c99a115185ae6a5",
"sha256:f64840e68483316eb58d82c376ad3585ca995e69e33b230436de0cdddf7363f9",
"sha256:f8f4b0a9e6683e43889852130595c8854d8ae237f2324a053cdd884de936aa9b",
"sha256:fc45a53219ed30a7f670a6d8c98527af0020e6fd4ee4c0a8fb59f147f06d816c"
],
"version": "==4.3.5"
}
},
"develop": {}

@ -12,6 +12,7 @@ import mozilla.appservices.places.BookmarkRoot
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
@ -31,6 +32,7 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
/**
* Tests for verifying basic functionality of bookmarks
*/
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17979")
class BookmarksTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.

@ -0,0 +1,102 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.ui
import androidx.core.net.toUri
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled
import org.mozilla.fenix.helpers.TestHelper.verifyUrl
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
/**
* Tests to verify some main UI flows with Network connection off
*
*/
class NoNetworkAccessStartupTests {
@get:Rule
val activityTestRule = HomeActivityTestRule(launchActivity = false)
@After
fun tearDown() {
// Restoring network connection
setNetworkEnabled(true)
}
@Test
// Based on STR from https://github.com/mozilla-mobile/fenix/issues/16886
fun noNetworkConnectionStartupTest() {
setNetworkEnabled(false)
activityTestRule.launchActivity(null)
homeScreen {
}.dismissOnboarding()
homeScreen {
verifyHomeScreen()
}
}
@Test
// Based on STR from https://github.com/mozilla-mobile/fenix/issues/16886
fun networkInterruptedFromBrowserToHomeTest() {
val url = "example.com"
activityTestRule.launchActivity(null)
navigationToolbar {
}.enterURLAndEnterToBrowser(url.toUri()) {}
setNetworkEnabled(false)
browserScreen {
}.goToHomescreen {
verifyHomeScreen()
}
}
@Test
fun testPageReloadAfterNetworkInterrupted() {
val url = "example.com"
activityTestRule.launchActivity(null)
navigationToolbar {
}.enterURLAndEnterToBrowser(url.toUri()) {}
setNetworkEnabled(false)
browserScreen {
}.openThreeDotMenu {
}.refreshPage {}
}
@Test
fun testSignInPageWithNoNetworkConnection() {
setNetworkEnabled(false)
activityTestRule.launchActivity(null)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openTurnOnSyncMenu {
tapOnUseEmailToSignIn()
verifyUrl(
"firefox.com",
"$packageName:id/mozac_browser_toolbar_url_view",
R.id.mozac_browser_toolbar_url_view
)
}
}
}

@ -5,22 +5,22 @@
package org.mozilla.fenix.ui
import android.view.View
import androidx.test.espresso.IdlingRegistry
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.ui.robots.navigationToolbar
import androidx.test.espresso.IdlingRegistry
import org.junit.Ignore
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.mDevice
import org.mozilla.fenix.ui.robots.navigationToolbar
/**
* Tests for verifying basic functionality of content context menus
@ -125,6 +125,15 @@ class ReaderViewTest {
toggleReaderView()
mDevice.waitForIdle()
}
browserScreen {
verifyPageContent(estimatedReadingTime)
}
navigationToolbar {
verifyCloseReaderViewDetected(true)
toggleReaderView()
mDevice.waitForIdle()
verifyReaderViewDetected(true)
}
if (!FeatureFlags.toolbarMenuFeature) {
browserScreen {
@ -134,12 +143,14 @@ class ReaderViewTest {
}.closeBrowserMenuToBrowser { }
}
navigationToolbar {
toggleReaderView()
mDevice.waitForIdle()
}.openThreeDotMenu {
verifyReaderViewAppearance(false)
}.close { }
if (!FeatureFlags.toolbarMenuFeature) {
navigationToolbar {
toggleReaderView()
mDevice.waitForIdle()
}.openThreeDotMenu {
verifyReaderViewAppearance(false)
}.close { }
}
}
@Test

@ -6,6 +6,7 @@ package org.mozilla.fenix.ui
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import okhttp3.mockwebserver.MockWebServer
import org.junit.Rule
import org.junit.Before
@ -15,6 +16,7 @@ import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.ui.robots.clickRateButtonGooglePlay
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.mDevice
/**
* Tests for verifying the main three dot menu options
@ -65,8 +67,10 @@ class SettingsAboutTest {
}.openSettings {
clickRateButtonGooglePlay()
verifyGooglePlayRedirect()
// press back to return to the app, or accept ToS if still visible
mDevice.pressBack()
dismissGooglePlayToS()
}
}
@Test
@ -79,3 +83,9 @@ class SettingsAboutTest {
}
}
}
private fun dismissGooglePlayToS() {
if (mDevice.findObject(UiSelector().textContains("Terms of Service")).exists()) {
mDevice.findObject(UiSelector().textContains("ACCEPT")).click()
}
}

@ -10,6 +10,7 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.FenixApplication
@ -105,6 +106,7 @@ class SettingsBasicsTest {
}
@Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17979")
fun toggleShowVisitedSitesAndBookmarks() {
// Bookmarks a few websites, toggles the history and bookmarks setting to off, then verifies if the visited and bookmarked websites do not show in the suggestions.
val page1 = getGenericAsset(mockWebServer, 1)

@ -322,6 +322,7 @@ class SmokeTest {
@Test
// Verifies the Bookmark button in a tab's 3 dot menu
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17979")
fun mainMenuBookmarkButtonTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -405,7 +406,7 @@ class SmokeTest {
}.goBackToHomeScreen {}
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingPage.url) {}
}.openTrackingProtectionTestPage(trackingPage.url, true) {}
enhancedTrackingProtection {
dismissTrackingOnboarding()
@ -630,7 +631,7 @@ class SmokeTest {
IdlingRegistry.getInstance().unregister(addonsListIdlingResource!!)
}.goBack {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionPage.url) {}
}.openTrackingProtectionTestPage(trackingProtectionPage.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
}.closeNotificationPopup {}
@ -980,6 +981,7 @@ class SmokeTest {
@Test
// Verifies that deleting a Bookmarks folder also removes the item from inside it.
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17799")
fun deleteNonEmptyBookmarkFolderTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -1056,6 +1058,33 @@ class SmokeTest {
}
}
@Test
fun selectTabsButtonVisibilityTest() {
homeScreen {
}.dismissOnboarding()
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
mDevice.waitForIdle()
}.openTabDrawer {
}.toggleToPrivateTabs {
}.openNewTab {
}.dismissSearchBar { }
homeScreen {
}.openTabDrawer {
}.toggleToNormalTabs {
verifySelectTabsButton()
}
}
@Test
fun privateTabsTrayWithOpenedTabTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)

@ -83,7 +83,7 @@ class StrictEnhancedTrackingProtectionTest {
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
}.openTrackingProtectionTestPage(trackingProtectionTest.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
@ -96,7 +96,7 @@ class StrictEnhancedTrackingProtectionTest {
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
}.openTrackingProtectionTestPage(trackingProtectionTest.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
@ -113,7 +113,7 @@ class StrictEnhancedTrackingProtectionTest {
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
}.openTrackingProtectionTestPage(trackingProtectionTest.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
@ -133,7 +133,7 @@ class StrictEnhancedTrackingProtectionTest {
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
}.openTrackingProtectionTestPage(trackingProtectionTest.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
@ -164,7 +164,7 @@ class StrictEnhancedTrackingProtectionTest {
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
}.openTrackingProtectionTestPage(trackingProtectionTest.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
@ -196,7 +196,7 @@ class StrictEnhancedTrackingProtectionTest {
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
}.openTrackingProtectionTestPage(trackingProtectionTest.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()

@ -150,7 +150,7 @@ class HomeScreenRobot {
fun confirmDeleteCollection() {
onView(allOf(withText("DELETE"))).click()
mDevice.waitNotNull(
findObject(By.res("org.mozilla.fenix.debug:id/no_collections_header")),
findObject(By.res("$packageName:id/no_collections_header")),
waitingTime
)
}

@ -11,7 +11,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.IdlingResourceTimeoutException
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.pressImeActionButton
import androidx.test.espresso.action.ViewActions.replaceText
@ -21,6 +21,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
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.withId
@ -60,12 +61,21 @@ class NavigationToolbarRobot {
fun verifyTabButtonShortcutMenuItems() = assertTabButtonShortcutMenuItems()
fun verifyReaderViewDetected(visible: Boolean = false): ViewInteraction =
fun verifyReaderViewDetected(visible: Boolean = false) =
assertReaderViewDetected(visible)
fun verifyCloseReaderViewDetected(visible: Boolean = false) =
assertCloseReaderViewDetected(visible)
fun typeSearchTerm(searchTerm: String) = awesomeBar().perform(typeText(searchTerm))
fun toggleReaderView() = readerViewToggle().click()
fun toggleReaderView() {
mDevice.findObject(UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_page_actions"))
.waitForExists(waitingTime)
readerViewToggle().click()
}
class Transition {
@ -73,15 +83,7 @@ class NavigationToolbarRobot {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
fun goBackToWebsite(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/toolbar")),
waitingTime
)
urlBar().click()
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_edit_url_view")),
waitingTime
)
openEditURLView()
clearAddressBar().click()
awesomeBar().check((matches(withText(containsString("")))))
goBackButton()
@ -96,14 +98,7 @@ class NavigationToolbarRobot {
): BrowserRobot.Transition {
sessionLoadedIdlingResource = SessionLoadedIdlingResource()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/toolbar")),
waitingTime
)
urlBar().click()
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_edit_url_view")),
waitingTime
)
openEditURLView()
awesomeBar().perform(replaceText(url.toString()), pressImeActionButton())
@ -122,19 +117,55 @@ class NavigationToolbarRobot {
return BrowserRobot.Transition()
}
fun openTrackingProtectionTestPage(url: Uri, etpEnabled: Boolean, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
sessionLoadedIdlingResource = SessionLoadedIdlingResource()
openEditURLView()
awesomeBar().perform(replaceText(url.toString()), pressImeActionButton())
runWithIdleRes(sessionLoadedIdlingResource) {
when (etpEnabled) {
true ->
try {
onView(withId(R.id.onboarding_message))
.check(matches(isDisplayed()))
} catch (e: IdlingResourceTimeoutException) {
openThreeDotMenu {
}.stopPageLoad {
val onboardingDisplayed =
mDevice.findObject(UiSelector().resourceId("$packageName:id/onboarding_message"))
.waitForExists(waitingTime)
if (!onboardingDisplayed) {
openThreeDotMenu {
}.refreshPage {}
}
}
}
false ->
try {
onView(withResourceName("browserLayout")).check(matches(isDisplayed()))
} catch (e: IdlingResourceTimeoutException) {
openThreeDotMenu {
}.stopPageLoad {
}.openThreeDotMenu {
}.refreshPage {}
}
}
}
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openTabCrashReporter(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
val crashUrl = "about:crashcontent"
sessionLoadedIdlingResource = SessionLoadedIdlingResource()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/toolbar")),
waitingTime
)
urlBar().click()
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_edit_url_view")),
waitingTime
)
openEditURLView()
awesomeBar().perform(replaceText(crashUrl), pressImeActionButton())
@ -277,6 +308,18 @@ fun clickUrlbar(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
return SearchRobot.Transition()
}
fun openEditURLView() {
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/toolbar")),
waitingTime
)
urlBar().click()
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_edit_url_view")),
waitingTime
)
}
private fun assertSuggestionsAreEqualTo(suggestionSize: Int) {
mDevice.waitForIdle()
onView(withId(R.id.awesome_bar)).check(suggestionsAreEqualTo(suggestionSize))
@ -312,7 +355,11 @@ private fun goBackButton() = mDevice.pressBack()
private fun readerViewToggle() =
onView(withParent(withId(R.id.mozac_browser_toolbar_page_actions)))
private fun assertReaderViewDetected(visible: Boolean) =
private fun assertReaderViewDetected(visible: Boolean) {
mDevice.findObject(UiSelector()
.description("Reader view"))
.waitForExists(waitingTime)
onView(
allOf(
withParent(withId(R.id.mozac_browser_toolbar_page_actions)),
@ -322,6 +369,23 @@ private fun assertReaderViewDetected(visible: Boolean) =
if (visible) matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
else ViewAssertions.doesNotExist()
)
}
private fun assertCloseReaderViewDetected(visible: Boolean) {
mDevice.findObject(UiSelector()
.description("Close reader view"))
.waitForExists(waitingTime)
onView(
allOf(
withParent(withId(R.id.mozac_browser_toolbar_page_actions)),
withContentDescription("Close reader view")
)
).check(
if (visible) matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
else ViewAssertions.doesNotExist()
)
}
inline fun runWithIdleRes(ir: IdlingResource?, pendingCheck: () -> Unit) {
try {

@ -13,27 +13,23 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
class NotificationRobot {
fun verifySystemNotificationExists(notificationMessage: String) {
fun notificationTray() = UiScrollable(
UiSelector().resourceId("com.android.systemui:id/notification_stack_scroller")
)
val notificationFound: Boolean
notificationFound = try {
notificationTray().getChildByText(
UiSelector().text(notificationMessage), notificationMessage, true
).exists()
} catch (e: UiObjectNotFoundException) {
false
}
if (!notificationFound) {
// swipe 2 times to expand the silent notifications on API 28 and higher, single-swipe doesn't do it
notificationTray().swipeUp(2)
val notification = mDevice.findObject(UiSelector().textContains(notificationMessage))
assertTrue(notification.exists())
}
var notificationFound = false
do {
try {
notificationFound = notificationTray().getChildByText(
UiSelector().text(notificationMessage), notificationMessage, true
).waitForExists(waitingTime)
assertTrue(notificationFound)
} catch (e: UiObjectNotFoundException) {
notificationTray().scrollForward()
mDevice.waitForIdle()
}
} while (!notificationFound)
}
fun verifySystemNotificationGone(notificationMessage: String) {

@ -77,6 +77,7 @@ class TabDrawerRobot {
fun verifyPrivateModeSelected() = assertPrivateModeSelected()
fun verifyNormalModeSelected() = assertNormalModeSelected()
fun verifyNewTabButton() = assertNewTabButton()
fun verifySelectTabsButton() = assertSelectTabsButton()
fun verifyTabTrayOverflowMenu(visibility: Boolean) = assertTabTrayOverflowButton(visibility)
fun verifyTabTrayIsClosed() = assertTabTrayDoesNotExist()
@ -137,7 +138,7 @@ class TabDrawerRobot {
fun snackBarButtonClick(expectedText: String) {
mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/snackbar_btn")
UiSelector().resourceId("$packageName:id/snackbar_btn")
).waitForExists(waitingTime)
onView(allOf(withId(R.id.snackbar_btn), withText(expectedText))).check(
matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
@ -391,6 +392,10 @@ private fun assertNewTabButton() =
onView(withId(R.id.new_tab_button))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertSelectTabsButton() =
onView(withText("Select tabs"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertNormalModeSelected() =
normalBrowsingButton()
.check(matches(ViewMatchers.isSelected()))

@ -280,6 +280,14 @@ class ThreeDotMenuMainRobot {
return BrowserRobot.Transition()
}
fun stopPageLoad(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.desc("Stop")), waitingTime)
stopLoadingButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun closeAllTabs(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
closeAllTabsButton().click()
@ -444,6 +452,8 @@ private fun refreshButton() = onView(ViewMatchers.withContentDescription("Refres
private fun assertRefreshButton() = refreshButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun stopLoadingButton() = onView(ViewMatchers.withContentDescription("Stop"))
private fun closeAllTabsButton() = onView(allOf(withText("Close all tabs"))).inRoot(RootMatchers.isPlatformPopup())
private fun assertCloseAllTabsButton() = closeAllTabsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

@ -18,6 +18,25 @@
<application
tools:replace="android:name"
android:name="org.mozilla.fenix.DebugFenixApplication" />
android:name="org.mozilla.fenix.DebugFenixApplication">
<activity android:name=".autofill.AutofillUnlockActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity android:name=".autofill.AutofillConfirmActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Translucent" />
<service
android:name=".autofill.AutofillService"
android:label="@string/app_name"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<intent-filter>
<action android:name="android.service.autofill.AutofillService"/>
</intent-filter>
</service>
</application>
</manifest>

@ -18,6 +18,10 @@
<!-- Needed to prompt the user to give permission to install a downloaded apk -->
<uses-permission-sdk-23 android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- Needed to interact with all apps installed on a device -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name=".FenixApplication"
android:allowBackup="false"
@ -142,6 +146,15 @@
android:exported="true"
android:excludeFromRecents="true" >
<!--
Respond to `Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_BROWSER)`
-->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.APP_BROWSER"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

@ -14,28 +14,39 @@ function collectLinks(urls) {
}
}
function sendLinks() {
function sendLinks(cookies) {
let urls = [];
collectLinks(urls);
let message = {
'url': document.location.href,
'urls': urls
'urls': urls,
'cookies': cookies
};
browser.runtime.sendNativeMessage("MozacBrowserAds", message);
}
function notify(message) {
sendLinks(message.cookies);
}
browser.runtime.onMessage.addListener(notify);
const events = ["pageshow", "load", "unload"];
var timeout;
const eventLogger = event => {
switch (event.type) {
case "load":
timeout = setTimeout(sendLinks, ADLINK_CHECK_TIMEOUT_MS);
timeout = setTimeout(() => {
browser.runtime.sendMessage({ "checkCookies": true });
}, ADLINK_CHECK_TIMEOUT_MS)
break;
case "pageshow":
if (event.persisted) {
timeout = setTimeout(sendLinks, ADLINK_CHECK_TIMEOUT_MS);
timeout = setTimeout(() => {
browser.runtime.sendMessage({ "checkCookies": true });
}, ADLINK_CHECK_TIMEOUT_MS)
}
break;
case "unload":

@ -0,0 +1,28 @@
/* 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/. */
browser.runtime.onMessage.addListener(notify);
function sendMessageToTabs(tabs, cookies) {
for (let tab of tabs) {
browser.tabs.sendMessage(
tab.id,
{ cookies }
);
}
}
function notify(message) {
if (message.checkCookies) {
browser.cookies.getAll({})
.then(cookies => {
browser.tabs.query({
currentWindow: true,
active: true
}).then(tabs => {
sendMessageToTabs(tabs, cookies);
});
});
}
}

@ -19,9 +19,20 @@
"run_at": "document_end"
}
],
"background": {
"scripts": ["adsBackground.js"]
},
"permissions": [
"geckoViewAddons",
"nativeMessaging",
"nativeMessagingFromContent"
"nativeMessagingFromContent",
"geckoViewAddons",
"nativeMessaging",
"nativeMessagingFromContent",
"webNavigation",
"webRequest",
"webRequestBlocking",
"cookies",
"*://*/*"
]
}

@ -19,6 +19,16 @@ object FeatureFlags {
*/
const val nimbusExperiments = false
/**
* Enables the Addresses autofill feature.
*/
val addressesFeature = true
/**
* Enables the Credit Cards autofill feature.
*/
val creditCardsFeature = true
/**
* Enables WebAuthn support.
*/
@ -28,4 +38,14 @@ object FeatureFlags {
* Shows new three-dot toolbar menu design.
*/
val toolbarMenuFeature = Config.channel.isDebug
/**
* Enables the tabs tray re-write with Synced Tabs.
*/
val tabsTrayRewrite = Config.channel.isDebug
/**
* Enables the updated icon set look and feel.
*/
val newIconSet = Config.channel.isNightlyOrDebug
}

@ -10,6 +10,7 @@ import android.os.Build.VERSION.SDK_INT
import android.os.StrictMode
import android.util.Log.INFO
import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.getSystemService
import androidx.work.Configuration.Builder
@ -23,7 +24,10 @@ import mozilla.appservices.Megazord
import mozilla.components.browser.state.action.SystemAction
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.isUnsupported
import mozilla.components.concept.push.PushProcessor
import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.service.glean.Glean
@ -39,10 +43,13 @@ import mozilla.components.support.rusthttp.RustHttpConfig
import mozilla.components.support.rustlog.RustLog
import mozilla.components.support.utils.logElapsedTime
import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.fenix.GleanMetrics.GleanBuildInfo
import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.components.metrics.SecurePrefsTelemetry
import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.ProfilerMarkerFactProcessor
import org.mozilla.fenix.perf.StartupTimeline
@ -73,7 +80,10 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
private set
override fun onCreate() {
val methodDurationTimerId = PerfStartup.applicationOnCreate.start() // DO NOT MOVE ANYTHING ABOVE HERE.
// We use start/stop instead of measure so we don't measure outside the main process.
val completeMethodDurationTimerId = PerfStartup.applicationOnCreate.start() // DO NOT MOVE ANYTHING ABOVE HERE.
val subsectionThroughGleanTimerId = PerfStartup.appOnCreateToGleanInit.start()
super.onCreate()
setupInAllProcesses()
@ -94,10 +104,12 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
initializeGlean()
}
PerfStartup.appOnCreateToGleanInit.stopAndAccumulate(subsectionThroughGleanTimerId)
setupInMainProcessOnly()
// We use start/stop instead of measure so we don't measure outside the main process.
PerfStartup.applicationOnCreate.stopAndAccumulate(methodDurationTimerId) // DO NOT MOVE ANYTHING BELOW HERE.
// DO NOT MOVE ANYTHING BELOW THIS stop CALL.
PerfStartup.applicationOnCreate.stopAndAccumulate(completeMethodDurationTimerId)
}
protected open fun initializeGlean() {
@ -112,7 +124,16 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
httpClient = ConceptFetchHttpUploader(
lazy(LazyThreadSafetyMode.NONE) { components.core.client }
)),
uploadEnabled = telemetryEnabled
uploadEnabled = telemetryEnabled,
buildInfo = GleanBuildInfo.buildInfo
)
// Set this early to guarantee it's in every ping from here on.
Metrics.distributionId.set(
when (Config.channel.isMozillaOnline) {
true -> "MozillaOnline"
false -> "Mozilla"
}
)
}
@ -126,48 +147,53 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
@CallSuper
open fun setupInMainProcessOnly() {
ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register()
PerfStartup.appOnCreateToMegazordInit.measureNoInline {
ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register()
run {
// Attention: Do not invoke any code from a-s in this scope.
val megazordSetup = setupMegazord()
run {
// Attention: Do not invoke any code from a-s in this scope.
val megazordSetup = setupMegazord()
setDayNightTheme()
components.strictMode.enableStrictMode(true)
warmBrowsersCache()
setDayNightTheme()
components.strictMode.enableStrictMode(true)
warmBrowsersCache()
// Make sure the engine is initialized and ready to use.
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
components.core.engine.warmUp()
}
initializeWebExtensionSupport()
restoreBrowserState()
restoreDownloads()
// Just to make sure it is impossible for any application-services pieces
// to invoke parts of itself that require complete megazord initialization
// before that process completes, we wait here, if necessary.
if (!megazordSetup.isCompleted) {
runBlockingIncrement { megazordSetup.await() }
// Make sure the engine is initialized and ready to use.
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
components.core.engine.warmUp()
}
initializeWebExtensionSupport()
restoreBrowserState()
restoreDownloads()
restoreLocale()
// Just to make sure it is impossible for any application-services pieces
// to invoke parts of itself that require complete megazord initialization
// before that process completes, we wait here, if necessary.
if (!megazordSetup.isCompleted) {
runBlockingIncrement { megazordSetup.await() }
}
}
}
setupLeakCanary()
startMetricsIfEnabled()
setupPush()
PerfStartup.appOnCreateToSetupInMain.measureNoInline {
setupLeakCanary()
startMetricsIfEnabled()
setupPush()
visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService())
registerActivityLifecycleCallbacks(visibilityLifecycleCallback)
visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService())
registerActivityLifecycleCallbacks(visibilityLifecycleCallback)
// Storage maintenance disabled, for now, as it was interfering with background migrations.
// See https://github.com/mozilla-mobile/fenix/issues/7227 for context.
// if ((System.currentTimeMillis() - settings().lastPlacesStorageMaintenance) > ONE_DAY_MILLIS) {
// runStorageMaintenance()
// }
// Storage maintenance disabled, for now, as it was interfering with background migrations.
// See https://github.com/mozilla-mobile/fenix/issues/7227 for context.
// if ((System.currentTimeMillis() - settings().lastPlacesStorageMaintenance) > ONE_DAY_MILLIS) {
// runStorageMaintenance()
// }
initVisualCompletenessQueueAndQueueTasks()
initVisualCompletenessQueueAndQueueTasks()
components.appStartupTelemetry.onFenixApplicationOnCreate()
components.appStartupTelemetry.onFenixApplicationOnCreate()
}
}
private fun restoreBrowserState() = GlobalScope.launch(Dispatchers.Main) {
@ -188,6 +214,10 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
components.useCases.downloadUseCases.restoreDownloads()
}
private fun restoreLocale() {
components.useCases.localeUseCases.restore()
}
private fun initVisualCompletenessQueueAndQueueTasks() {
val queue = components.performance.visualCompletenessQueue.queue
@ -454,7 +484,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
},
onExtensionsLoaded = { extensions ->
components.addonUpdater.registerForFutureUpdates(extensions)
components.supportedAddonsChecker.registerForChecks()
subscribeForNewAddonsIfNeeded(components.supportedAddonsChecker, extensions)
},
onUpdatePermissionRequest = components.addonUpdater::onUpdatePermissionRequest
)
@ -463,6 +493,21 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
}
@VisibleForTesting
internal fun subscribeForNewAddonsIfNeeded(
checker: DefaultSupportedAddonsChecker,
installedExtensions: List<WebExtension>
) {
val hasUnsupportedAddons = installedExtensions.any { it.isUnsupported() }
if (hasUnsupportedAddons) {
checker.registerForChecks()
} else {
// As checks are a persistent subscriptions, we have to make sure
// we remove any previous subscriptions.
checker.unregisterForChecks()
}
}
protected fun recordOnInit() {
// This gets called by more than one process. Ideally we'd only run this in the main process
// but the code to check which process we're in crashes because the Context isn't valid yet.

@ -77,6 +77,7 @@ import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExcepti
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
@ -164,7 +165,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
private lateinit var navigationToolbar: Toolbar
final override fun onCreate(savedInstanceState: Bundle?): Unit = PerfStartup.homeActivityOnCreate.measure {
final override fun onCreate(savedInstanceState: Bundle?): Unit = PerfStartup.homeActivityOnCreate.measureNoInline {
// DO NOT MOVE ANYTHING ABOVE THIS addMarker CALL.
components.core.engine.profiler?.addMarker("Activity.onCreate", "HomeActivity")
@ -321,7 +322,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
}
override fun onStart() {
override fun onStart() = PerfStartup.homeActivityOnStart.measureNoInline {
super.onStart()
// Diagnostic breadcrumb for "Display already aquired" crash:
@ -359,10 +360,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
window.addFlags(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
lifecycleScope.launch(IO) {
components.core.bookmarksStorage.getTree(BookmarkRoot.Root.id, true)?.let {
val desktopRootNode = DesktopFolders(

@ -24,7 +24,7 @@ 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
import org.mozilla.fenix.GleanMetrics.Engine as EngineMetrics
import org.mozilla.fenix.GleanMetrics.EngineTab as EngineMetrics
/**
* [Middleware] to record telemetry in response to [BrowserAction]s.
@ -138,24 +138,24 @@ class TelemetryMiddleware(
}
val isSelected = tab.id == state.selectedTabId
val ageNanos = tab.engineState.ageNanos()
val age = tab.engineState.age()
// Increment the counter of killed foreground/background tabs
val tabKillLabel = if (isSelected) { "foreground" } else { "background" }
EngineMetrics.tabKills[tabKillLabel].add()
EngineMetrics.kills[tabKillLabel].add()
// Record the age of the engine session of the killed foreground/background tab.
if (isSelected && ageNanos != null) {
EngineMetrics.killForegroundAge.setRawNanos(ageNanos)
} else if (ageNanos != null) {
EngineMetrics.killBackgroundAge.setRawNanos(ageNanos)
if (isSelected && age != null) {
EngineMetrics.killForegroundAge.accumulateSamples(listOf(age).toLongArray())
} else if (age != null) {
EngineMetrics.killBackgroundAge.accumulateSamples(listOf(age).toLongArray())
}
}
}
@Suppress("MagicNumber")
private fun EngineState.ageNanos(): Long? {
private fun EngineState.age(): Long? {
val timestamp = (timestamp ?: return null)
val now = Clock.elapsedRealtime()
return (now - timestamp) * 1_000_000
return (now - timestamp)
}

@ -0,0 +1,19 @@
/* 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.autofill
import android.os.Build
import androidx.annotation.RequiresApi
import mozilla.components.feature.autofill.AutofillConfiguration
import mozilla.components.feature.autofill.ui.AbstractAutofillConfirmActivity
import org.mozilla.fenix.ext.components
/**
* Activity responsible for asking the user to confirm before auto-filling a third-party app.
*/
@RequiresApi(Build.VERSION_CODES.O)
class AutofillConfirmActivity : AbstractAutofillConfirmActivity() {
override val configuration: AutofillConfiguration by lazy { components.autofillConfiguration }
}

@ -0,0 +1,19 @@
/* 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.autofill
import android.os.Build
import androidx.annotation.RequiresApi
import mozilla.components.feature.autofill.AbstractAutofillService
import mozilla.components.feature.autofill.AutofillConfiguration
import org.mozilla.fenix.ext.components
/**
* Service responsible for implementing Android's Autofill framework.
*/
@RequiresApi(Build.VERSION_CODES.O)
class AutofillService : AbstractAutofillService() {
override val configuration: AutofillConfiguration by lazy { components.autofillConfiguration }
}

@ -0,0 +1,19 @@
/* 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.autofill
import android.os.Build
import androidx.annotation.RequiresApi
import mozilla.components.feature.autofill.AutofillConfiguration
import mozilla.components.feature.autofill.ui.AbstractAutofillUnlockActivity
import org.mozilla.fenix.ext.components
/**
* Activity responsible for unlocking the autofill service by asking the user to verify.
*/
@RequiresApi(Build.VERSION_CODES.O)
class AutofillUnlockActivity : AbstractAutofillUnlockActivity() {
override val configuration: AutofillConfiguration by lazy { components.autofillConfiguration }
}

@ -80,7 +80,6 @@ import mozilla.components.service.sync.logins.DefaultLoginValidationDelegate
import mozilla.components.support.base.feature.PermissionsFeature
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
@ -110,7 +109,6 @@ import org.mozilla.fenix.downloads.DynamicDownloadDialog
import org.mozilla.fenix.ext.accessibilityManager
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.enterToImmersiveMode
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.metrics
@ -129,6 +127,10 @@ import java.lang.ref.WeakReference
import mozilla.components.feature.session.behavior.EngineViewBrowserToolbarBehavior
import mozilla.components.feature.webauthn.WebAuthnFeature
import mozilla.components.support.base.feature.ActivityResultHandler
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.ext.enterToImmersiveMode
import org.mozilla.fenix.ext.exitImmersiveModeIfNeeded
import org.mozilla.fenix.ext.measureNoInline
import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition
/**
@ -197,7 +199,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
): View = PerfStartup.baseBfragmentOnCreateView.measureNoInline {
customTabSessionId = requireArguments().getString(EXTRA_SESSION_ID)
// Diagnostic breadcrumb for "Display already aquired" crash:
@ -220,10 +222,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
)
}
return view
view
}
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) =
PerfStartup.baseBfragmentOnViewCreated.measureNoInline { // weird indentation to avoid breaking blame.
initializeUI(view)
if (customTabSessionId == null) {
@ -240,6 +243,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
}
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
Unit
}
private fun initializeUI(view: View) {
@ -294,7 +298,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
thumbnailsFeature.get()?.requestScreenshot()
findNavController().nav(
R.id.browserFragment,
BrowserFragmentDirections.actionGlobalTabTrayDialogFragment()
getTrayDirection(context)
)
},
onCloseTab = { closedSession ->
@ -474,13 +478,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
didFail = downloadJobStatus == DownloadState.Status.FAILED,
tryAgain = downloadFeature::tryAgain,
onCannotOpenFile = {
FenixSnackbar.make(
view = view.browserLayout,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = true
)
.setText(context.getString(R.string.mozac_feature_downloads_could_not_open_file))
.show()
showCannotOpenFileError(view.browserLayout, context, it)
},
view = view.viewDynamicDownloadDialog,
toolbarHeight = toolbarHeight,
@ -783,16 +781,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
}
}
val onCannotOpenFile = {
FenixSnackbar.make(
view = view.browserLayout,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = true
)
.setText(context.getString(R.string.mozac_feature_downloads_could_not_open_file))
.show()
}
val onDismiss: () -> Unit =
{ sharedViewModel.downloadDialogState.remove(sessionId) }
@ -802,7 +790,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
metrics = requireComponents.analytics.metrics,
didFail = savedDownloadState.second,
tryAgain = onTryAgain,
onCannotOpenFile = onCannotOpenFile,
onCannotOpenFile = {
showCannotOpenFileError(view.browserLayout, context, it)
},
view = view.viewDynamicDownloadDialog,
toolbarHeight = toolbarHeight,
onDismiss = onDismiss
@ -1280,6 +1270,30 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
)
}
private fun showCannotOpenFileError(
view: View,
context: Context,
downloadState: DownloadState
) {
FenixSnackbar.make(
view = view,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = true
).setText(DynamicDownloadDialog.getCannotOpenFileErrorMessage(context, downloadState))
.show()
}
/**
* Retrieves the correct tray direction while using a feature flag.
*
* Remove this when [FeatureFlags.tabsTrayRewrite] is removed.
*/
private fun getTrayDirection(context: Context) = if (context.settings().tabsTrayRewrite) {
BrowserFragmentDirections.actionGlobalTabsTrayFragment()
} else {
BrowserFragmentDirections.actionGlobalTabTrayDialogFragment()
}
companion object {
private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id"
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1

@ -98,7 +98,8 @@ class Analytics(
AdjustMetricsService(context as Application)
),
isDataTelemetryEnabled = { context.settings().isTelemetryEnabled },
isMarketingDataTelemetryEnabled = { context.settings().isMarketingTelemetryEnabled }
isMarketingDataTelemetryEnabled = { context.settings().isMarketingTelemetryEnabled },
context.settings()
)
}

@ -8,11 +8,13 @@ import android.app.Application
import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
import com.google.android.play.core.review.ReviewManagerFactory
import mozilla.components.feature.addons.AddonManager
import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
import mozilla.components.feature.addons.migration.SupportedAddonsChecker
import mozilla.components.feature.addons.update.AddonUpdater
import mozilla.components.feature.addons.update.DefaultAddonUpdater
import mozilla.components.feature.autofill.AutofillConfiguration
import mozilla.components.feature.sitepermissions.SitePermissionsStorage
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.migration.state.MigrationStore
@ -20,6 +22,8 @@ import io.github.forkmaintainers.iceraven.components.PagedAddonCollectionProvide
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.autofill.AutofillUnlockActivity
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.components.metrics.AppStartupTelemetry
import org.mozilla.fenix.ext.components
@ -67,11 +71,10 @@ class Components(private val context: Context) {
core.topSitesStorage
)
}
@Suppress("Deprecation")
val intentProcessors by lazyMonitored {
IntentProcessors(
context,
core.sessionManager,
core.store,
useCases.sessionUseCases,
useCases.tabsUseCases,
@ -155,8 +158,19 @@ class Components(private val context: Context) {
val reviewPromptController by lazyMonitored {
ReviewPromptController(
context,
FenixReviewSettings(settings)
manager = ReviewManagerFactory.create(context),
reviewSettings = FenixReviewSettings(settings)
)
}
val autofillConfiguration by lazyMonitored {
AutofillConfiguration(
storage = core.passwordsStorage,
publicSuffixList = publicSuffixList,
unlockActivity = AutofillUnlockActivity::class.java,
confirmActivity = AutofillConfiguration::class.java,
applicationName = context.getString(R.string.app_name),
httpClient = core.client
)
}
}

@ -56,6 +56,7 @@ import mozilla.components.service.digitalassetlinks.local.StatementApi
import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
import mozilla.components.service.location.LocationService
import mozilla.components.service.location.MozillaLocationService
import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage
import mozilla.components.service.sync.logins.SyncableLoginsStorage
import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.AppRequestInterceptor
@ -288,6 +289,7 @@ class Core(
val lazyHistoryStorage = lazyMonitored { PlacesHistoryStorage(context, crashReporter) }
val lazyBookmarksStorage = lazyMonitored { PlacesBookmarksStorage(context) }
val lazyPasswordsStorage = lazyMonitored { SyncableLoginsStorage(context, passwordsEncryptionKey) }
val lazyAutofillStorage = lazyMonitored { AutofillCreditCardsAddressesStorage(context) }
/**
* The storage component to sync and persist tabs in a Firefox Sync account.
@ -298,6 +300,7 @@ class Core(
val historyStorage: PlacesHistoryStorage get() = lazyHistoryStorage.value
val bookmarksStorage: PlacesBookmarksStorage get() = lazyBookmarksStorage.value
val passwordsStorage: SyncableLoginsStorage get() = lazyPasswordsStorage.value
val autofillStorage: AutofillCreditCardsAddressesStorage get() = lazyAutofillStorage.value
val tabCollectionStorage by lazyMonitored {
TabCollectionStorage(

@ -5,7 +5,6 @@
package org.mozilla.fenix.components
import android.content.Context
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.customtabs.CustomTabIntentProcessor
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
@ -32,7 +31,6 @@ import org.mozilla.fenix.utils.Mockable
@Suppress("LongParameterList")
class IntentProcessors(
private val context: Context,
private val sessionManager: SessionManager,
private val store: BrowserStore,
private val sessionUseCases: SessionUseCases,
private val tabsUseCases: TabsUseCases,
@ -74,12 +72,12 @@ class IntentProcessors(
store = customTabsStore
),
WebAppIntentProcessor(store, tabsUseCases.addTab, sessionUseCases.loadUrl, manifestStorage),
FennecWebAppIntentProcessor(context, sessionManager, sessionUseCases.loadUrl, manifestStorage)
FennecWebAppIntentProcessor(context, tabsUseCases.addTab, manifestStorage)
)
}
val fennecPageShortcutIntentProcessor by lazyMonitored {
FennecBookmarkShortcutsIntentProcessor(sessionManager, sessionUseCases.loadUrl)
FennecBookmarkShortcutsIntentProcessor(tabsUseCases.addTab)
}
val migrationIntentProcessor by lazyMonitored {

@ -5,8 +5,10 @@
package org.mozilla.fenix.components
import android.app.Activity
import android.content.Context
import androidx.annotation.VisibleForTesting
import com.google.android.play.core.review.ReviewManager
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.withContext
import org.mozilla.fenix.utils.Settings
/**
@ -38,10 +40,19 @@ class FenixReviewSettings(
* Controls the Review Prompt behavior.
*/
class ReviewPromptController(
private val context: Context,
private val manager: ReviewManager,
private val reviewSettings: ReviewSettings,
private val timeNowInMillis: () -> Long = { System.currentTimeMillis() },
private val tryPromptReview: suspend (Activity) -> Unit = { _ ->
private val tryPromptReview: suspend (Activity) -> Unit = { activity ->
val flow = manager.requestReviewFlow()
withContext(Main) {
flow.addOnCompleteListener {
if (it.isSuccessful) {
manager.launchReviewFlow(activity, it.result)
}
}
}
}
) {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)

@ -22,6 +22,7 @@ import mozilla.components.feature.tabs.CustomTabsUseCases
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.TopSitesStorage
import mozilla.components.feature.top.sites.TopSitesUseCases
import mozilla.components.support.locale.LocaleUseCases
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.Mockable
@ -88,4 +89,9 @@ class UseCases(
* Use cases that provide top sites management.
*/
val topSitesUseCase by lazyMonitored { TopSitesUseCases(topSitesStorage) }
/**
* Use cases that handle locale management.
*/
val localeUseCases by lazyMonitored { LocaleUseCases(store) }
}

@ -74,6 +74,10 @@ sealed class Event {
object HistoryOpened : Event()
object HistoryItemShared : Event()
object HistoryItemOpened : Event()
object HistoryOpenedInNewTab : Event()
object HistoryOpenedInNewTabs : Event()
object HistoryOpenedInPrivateTab : Event()
object HistoryOpenedInPrivateTabs : Event()
object HistoryItemRemoved : Event()
object HistoryAllItemsRemoved : Event()
object ReaderModeAvailable : Event()
@ -212,8 +216,13 @@ sealed class Event {
object ContextMenuSelectAllTapped : Event()
object ContextMenuShareTapped : Event()
object HaveTopSites : Event()
object HaveNoTopSites : Event()
object SyncedTabSuggestionClicked : Event()
object BookmarkSuggestionClicked : Event()
object ClipboardSuggestionClicked : Event()
object HistorySuggestionClicked : Event()
object SearchActionClicked : Event()
object SearchSuggestionClicked : Event()
object OpenedTabSuggestionClicked : Event()
// Interaction events with extras
@ -523,9 +532,9 @@ sealed class Event {
get() = providerName
}
data class SearchAdClicked(val providerName: String) : Event() {
data class SearchAdClicked(val keyName: String) : Event() {
val label: String
get() = providerName
get() = keyName
}
data class SearchInContent(val keyName: String) : Event() {

@ -12,12 +12,12 @@ import mozilla.components.service.fxa.manager.SyncEnginesStorage
import mozilla.components.service.glean.Glean
import mozilla.components.service.glean.private.NoExtraKeys
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.Config
import org.mozilla.fenix.GleanMetrics.AboutPage
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.AndroidKeystoreExperiment
import org.mozilla.fenix.GleanMetrics.AppTheme
import org.mozilla.fenix.GleanMetrics.Autoplay
import org.mozilla.fenix.GleanMetrics.Awesomebar
import org.mozilla.fenix.GleanMetrics.BannerOpenInApp
import org.mozilla.fenix.GleanMetrics.BookmarksManagement
import org.mozilla.fenix.GleanMetrics.BrowserSearch
@ -54,6 +54,7 @@ import org.mozilla.fenix.GleanMetrics.SearchSuggestions
import org.mozilla.fenix.GleanMetrics.SearchWidget
import org.mozilla.fenix.GleanMetrics.SyncAccount
import org.mozilla.fenix.GleanMetrics.SyncAuth
import org.mozilla.fenix.GleanMetrics.SyncedTabs
import org.mozilla.fenix.GleanMetrics.Tab
import org.mozilla.fenix.GleanMetrics.Tabs
import org.mozilla.fenix.GleanMetrics.TabsTray
@ -68,6 +69,7 @@ import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.fenix.utils.Settings
private class EventWrapper<T : Enum<T>>(
private val recorder: ((Map<T, String>?) -> Unit),
@ -190,6 +192,9 @@ private val Event.wrapper: EventWrapper<*>?
{ Events.browserMenuAction.record(it) },
{ Events.browserMenuActionKeys.valueOf(it) }
)
is Event.OpenedBookmark -> EventWrapper<NoExtraKeys>(
{ BookmarksManagement.open.record(it) }
)
is Event.OpenedBookmarkInNewTab -> EventWrapper<NoExtraKeys>(
{ BookmarksManagement.openInNewTab.record(it) }
)
@ -303,6 +308,18 @@ private val Event.wrapper: EventWrapper<*>?
is Event.HistoryItemOpened -> EventWrapper<NoExtraKeys>(
{ History.openedItem.record(it) }
)
is Event.HistoryOpenedInNewTab -> EventWrapper<NoExtraKeys>(
{ History.openedItemInNewTab.record(it) }
)
is Event.HistoryOpenedInNewTabs -> EventWrapper<NoExtraKeys>(
{ History.openedItemsInNewTabs.record(it) }
)
is Event.HistoryOpenedInPrivateTab -> EventWrapper<NoExtraKeys>(
{ History.openedItemInPrivateTab.record(it) }
)
is Event.HistoryOpenedInPrivateTabs -> EventWrapper<NoExtraKeys>(
{ History.openedItemsInPrivateTabs.record(it) }
)
is Event.HistoryItemRemoved -> EventWrapper<NoExtraKeys>(
{ History.removed.record(it) }
)
@ -734,13 +751,6 @@ private val Event.wrapper: EventWrapper<*>?
Event.HaveNoOpenTabs -> EventWrapper<NoExtraKeys>(
{ Metrics.hasOpenTabs.set(false) }
)
Event.HaveTopSites -> EventWrapper<NoExtraKeys>(
{ Metrics.hasTopSites.set(true) }
)
Event.HaveNoTopSites -> EventWrapper<NoExtraKeys>(
{ Metrics.hasTopSites.set(false) }
)
is Event.BannerOpenInAppDisplayed -> EventWrapper<NoExtraKeys>(
{ BannerOpenInApp.displayed.record(it) }
)
@ -750,6 +760,51 @@ private val Event.wrapper: EventWrapper<*>?
is Event.BannerOpenInAppGoToSettings -> EventWrapper<NoExtraKeys>(
{ BannerOpenInApp.goToSettings.record(it) }
)
is Event.SyncedTabSuggestionClicked -> EventWrapper<NoExtraKeys>(
{ SyncedTabs.syncedTabsSuggestionClicked.record(it) }
)
is Event.BookmarkSuggestionClicked -> EventWrapper<NoExtraKeys>(
{ Awesomebar.bookmarkSuggestionClicked.record(it) }
)
is Event.ClipboardSuggestionClicked -> EventWrapper<NoExtraKeys>(
{ Awesomebar.clipboardSuggestionClicked.record(it) }
)
is Event.HistorySuggestionClicked -> EventWrapper<NoExtraKeys>(
{ Awesomebar.historySuggestionClicked.record(it) }
)
is Event.SearchActionClicked -> EventWrapper<NoExtraKeys>(
{ Awesomebar.searchActionClicked.record(it) }
)
is Event.SearchSuggestionClicked -> EventWrapper<NoExtraKeys>(
{ Awesomebar.searchSuggestionClicked.record(it) }
)
is Event.OpenedTabSuggestionClicked -> EventWrapper<NoExtraKeys>(
{ Awesomebar.openedTabSuggestionClicked.record(it) }
)
is Event.SecurePrefsExperimentFailure -> EventWrapper(
{ AndroidKeystoreExperiment.experimentFailure.record(it) },
{ AndroidKeystoreExperiment.experimentFailureKeys.valueOf(it) }
)
is Event.SecurePrefsGetFailure -> EventWrapper(
{ AndroidKeystoreExperiment.getFailure.record(it) },
{ AndroidKeystoreExperiment.getFailureKeys.valueOf(it) }
)
is Event.SecurePrefsGetSuccess -> EventWrapper(
{ AndroidKeystoreExperiment.getResult.record(it) },
{ AndroidKeystoreExperiment.getResultKeys.valueOf(it) }
)
is Event.SecurePrefsWriteFailure -> EventWrapper(
{ AndroidKeystoreExperiment.writeFailure.record(it) },
{ AndroidKeystoreExperiment.writeFailureKeys.valueOf(it) }
)
is Event.SecurePrefsWriteSuccess -> EventWrapper<NoExtraKeys>(
{ AndroidKeystoreExperiment.writeSuccess.record(it) }
)
is Event.SecurePrefsReset -> EventWrapper<NoExtraKeys>(
{ AndroidKeystoreExperiment.reset.record(it) }
)
is Event.SecurePrefsExperimentFailure -> EventWrapper(
{ AndroidKeystoreExperiment.experimentFailure.record(it) },
@ -776,7 +831,6 @@ private val Event.wrapper: EventWrapper<*>?
// Don't record other events in Glean:
is Event.AddBookmark -> null
is Event.OpenedBookmark -> null
is Event.OpenedAppFirstRun -> null
is Event.InteractWithSearchURLArea -> null
is Event.ClearedPrivateData -> null
@ -822,11 +876,20 @@ class GleanMetricsService(
// setStartupMetrics is not a fast function. It does not need to be done before we can consider
// ourselves initialized. So, let's do it, well, later.
setStartupMetrics()
setStartupMetrics(context.settings())
}
}
internal fun setStartupMetrics() {
/**
* This function is called before the metrics ping is sent. Part of this function depends on
* shared preferences to be updated so the correct value is sent with the metrics ping.
*
* The reason we're using shared preferences to track some of these values is due to the
* limitations of the metrics ping. Events are only sent in a metrics ping if the user have made
* changes between each ping. However, in some cases we want current values to be sent even if
* the user have not changed anything between pings.
*/
internal fun setStartupMetrics(settings: Settings) {
setPreferenceMetrics()
with(Metrics) {
defaultBrowser.set(browsersCache.all(context).isDefaultBrowser)
@ -834,55 +897,60 @@ class GleanMetricsService(
defaultMozBrowser.set(it)
}
distributionId.set(
when (Config.channel.isMozillaOnline) {
true -> "MozillaOnline"
false -> "Mozilla"
}
)
mozillaProducts.set(mozillaProductDetector.getInstalledMozillaProducts(context))
adjustCampaign.set(context.settings().adjustCampaignId)
adjustAdGroup.set(context.settings().adjustAdGroup)
adjustCreative.set(context.settings().adjustCreative)
adjustNetwork.set(context.settings().adjustNetwork)
adjustCampaign.set(settings.adjustCampaignId)
adjustAdGroup.set(settings.adjustAdGroup)
adjustCreative.set(settings.adjustCreative)
adjustNetwork.set(settings.adjustNetwork)
searchWidgetInstalled.set(context.settings().searchWidgetInstalled)
searchWidgetInstalled.set(settings.searchWidgetInstalled)
val openTabsCount = context.settings().openTabsCount
val openTabsCount = settings.openTabsCount
hasOpenTabs.set(openTabsCount > 0)
if (openTabsCount > 0) {
tabsOpenCount.add(openTabsCount)
}
val topSitesSize = context.settings().topSitesSize
val topSitesSize = settings.topSitesSize
hasTopSites.set(topSitesSize > 0)
if (topSitesSize > 0) {
topSitesCount.add(topSitesSize)
}
val desktopBookmarksSize = context.settings().desktopBookmarksSize
val installedAddonSize = settings.installedAddonsCount
Addons.hasInstalledAddons.set(installedAddonSize > 0)
if (installedAddonSize > 0) {
Addons.installedAddons.set(settings.installedAddonsList.split(','))
}
val enabledAddonSize = settings.enabledAddonsCount
Addons.hasEnabledAddons.set(enabledAddonSize > 0)
if (enabledAddonSize > 0) {
Addons.enabledAddons.set(settings.enabledAddonsList.split(','))
}
val desktopBookmarksSize = settings.desktopBookmarksSize
hasDesktopBookmarks.set(desktopBookmarksSize > 0)
if (desktopBookmarksSize > 0) {
desktopBookmarksCount.add(desktopBookmarksSize)
}
val mobileBookmarksSize = context.settings().mobileBookmarksSize
val mobileBookmarksSize = settings.mobileBookmarksSize
hasMobileBookmarks.set(mobileBookmarksSize > 0)
if (mobileBookmarksSize > 0) {
mobileBookmarksCount.add(mobileBookmarksSize)
}
toolbarPosition.set(
when (context.settings().toolbarPosition) {
when (settings.toolbarPosition) {
ToolbarPosition.BOTTOM -> Event.ToolbarPositionChanged.Position.BOTTOM.name
ToolbarPosition.TOP -> Event.ToolbarPositionChanged.Position.TOP.name
}
)
tabViewSetting.set(context.settings().getTabViewPingString())
closeTabSetting.set(context.settings().getTabTimeoutPingString())
tabViewSetting.set(settings.getTabViewPingString())
closeTabSetting.set(settings.getTabTimeoutPingString())
}
store.value.waitForSelectedOrDefaultSearchEngine { searchEngine ->

@ -10,6 +10,7 @@ import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts
import mozilla.components.browser.menu.facts.BrowserMenuFacts
import mozilla.components.browser.toolbar.facts.ToolbarFacts
import mozilla.components.concept.awesomebar.AwesomeBar
import mozilla.components.feature.awesomebar.facts.AwesomeBarFacts
import mozilla.components.feature.awesomebar.provider.BookmarksStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider
@ -22,6 +23,7 @@ import mozilla.components.feature.findinpage.facts.FindInPageFacts
import mozilla.components.feature.media.facts.MediaFacts
import mozilla.components.feature.prompts.dialog.LoginDialogFacts
import mozilla.components.feature.pwa.ProgressiveWebAppFacts
import mozilla.components.feature.syncedtabs.facts.SyncedTabsFacts
import mozilla.components.feature.top.sites.facts.TopSitesFacts
import mozilla.components.lib.dataprotect.SecurePrefsReliabilityExperiment
import mozilla.components.support.base.Component
@ -32,9 +34,9 @@ import mozilla.components.support.base.facts.Facts
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.webextensions.facts.WebExtensionFacts
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.PerfAwesomebar
import org.mozilla.fenix.search.awesomebar.ShortcutsSuggestionProvider
import org.mozilla.fenix.utils.Settings
interface MetricController {
fun start(type: MetricServiceType)
@ -45,13 +47,15 @@ interface MetricController {
fun create(
services: List<MetricsService>,
isDataTelemetryEnabled: () -> Boolean,
isMarketingDataTelemetryEnabled: () -> Boolean
isMarketingDataTelemetryEnabled: () -> Boolean,
settings: Settings
): MetricController {
return if (BuildConfig.TELEMETRY) {
ReleaseMetricController(
services,
isDataTelemetryEnabled,
isMarketingDataTelemetryEnabled
isMarketingDataTelemetryEnabled,
settings
)
} else DebugMetricController()
}
@ -81,7 +85,8 @@ internal class DebugMetricController(
internal class ReleaseMetricController(
private val services: List<MetricsService>,
private val isDataTelemetryEnabled: () -> Boolean,
private val isMarketingDataTelemetryEnabled: () -> Boolean
private val isMarketingDataTelemetryEnabled: () -> Boolean,
private val settings: Settings
) : MetricController {
private var initialized = mutableSetOf<MetricServiceType>()
@ -211,16 +216,16 @@ internal class ReleaseMetricController(
Component.SUPPORT_WEBEXTENSIONS to WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED -> {
metadata?.get("installed")?.let { installedAddons ->
if (installedAddons is List<*>) {
Addons.installedAddons.set(installedAddons.map { it.toString() })
Addons.hasInstalledAddons.set(installedAddons.size > 0)
settings.installedAddonsCount = installedAddons.size
settings.installedAddonsList = installedAddons.joinToString(",")
Leanplum.setUserAttributes(mapOf("installed_addons" to installedAddons.size))
}
}
metadata?.get("enabled")?.let { enabledAddons ->
if (enabledAddons is List<*>) {
Addons.enabledAddons.set(enabledAddons.map { it.toString() })
Addons.hasEnabledAddons.set(enabledAddons.size > 0)
settings.enabledAddonsCount = enabledAddons.size
settings.enabledAddonsList = enabledAddons.joinToString()
Leanplum.setUserAttributes(mapOf("enabled_addons" to enabledAddons.size))
}
}
@ -261,14 +266,32 @@ internal class ReleaseMetricController(
// Do nothing
}
return if (count > 0) {
Event.HaveTopSites
} else {
Event.HaveNoTopSites
}
settings.topSitesSize = count
}
null
}
Component.FEATURE_SYNCEDTABS to SyncedTabsFacts.Items.SYNCED_TABS_SUGGESTION_CLICKED -> {
Event.SyncedTabSuggestionClicked
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.BOOKMARK_SUGGESTION_CLICKED -> {
Event.BookmarkSuggestionClicked
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.CLIPBOARD_SUGGESTION_CLICKED -> {
Event.ClipboardSuggestionClicked
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.HISTORY_SUGGESTION_CLICKED -> {
Event.HistorySuggestionClicked
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.SEARCH_ACTION_CLICKED -> {
Event.SearchActionClicked
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.SEARCH_SUGGESTION_CLICKED -> {
Event.SearchSuggestionClicked
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.OPENED_TAB_SUGGESTION_CLICKED -> {
Event.OpenedTabSuggestionClicked
}
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.EXPERIMENT -> {
Event.SecurePrefsExperimentFailure(metadata?.get("javaClass") as String? ?: "null")
}
@ -289,6 +312,7 @@ internal class ReleaseMetricController(
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.RESET -> {
Event.SecurePrefsReset
}
else -> null
}

@ -143,7 +143,7 @@ class DefaultBrowserToolbarMenuController(
deleteAndQuit(activity, scope, snackbar)
}
is ToolbarMenu.Item.ReaderModeAppearance -> {
is ToolbarMenu.Item.CustomizeReaderView -> {
readerModeController.showControls()
metrics.track(Event.ReaderModeAppearanceOpened)
}
@ -361,7 +361,7 @@ class DefaultBrowserToolbarMenuController(
is ToolbarMenu.Item.OpenInFenix -> Event.BrowserMenuItemTapped.Item.OPEN_IN_FENIX
is ToolbarMenu.Item.InstallToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN
is ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT
is ToolbarMenu.Item.ReaderModeAppearance ->
is ToolbarMenu.Item.CustomizeReaderView ->
Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE
is ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP
// todo === End ===

@ -87,7 +87,7 @@ class BrowserToolbarView(
view.display.setOnUrlLongClickListener {
ToolbarPopupWindow.show(
WeakReference(view),
customTabSession,
customTabSession?.id,
interactor::onBrowserToolbarPasteAndGo,
interactor::onBrowserToolbarPaste
)

@ -63,10 +63,11 @@ class DefaultToolbarMenu(
val isPinningSupported: Boolean
) : ToolbarMenu {
private var currentUrlIsBookmarked = false
private var isCurrentUrlBookmarked = false
private var isBookmarkedJob: Job? = null
private val selectedSession: TabSessionState? get() = store.state.selectedTab
private val isTopToolbarSelected = shouldReverseItems
private val selectedSession: TabSessionState?
get() = store.state.selectedTab
override val menuBuilder by lazy {
WebExtensionBrowserMenuBuilder(
@ -146,24 +147,28 @@ class DefaultToolbarMenu(
registerForIsBookmarkedUpdates()
val bookmark = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = R.drawable.ic_bookmark_filled,
primaryContentDescription = context.getString(R.string.browser_menu_edit_bookmark),
primaryImageTintResource = primaryTextColor(),
// TwoStateButton.isInPrimaryState must be synchronous, and checking bookmark state is
// relatively slow. The best we can do here is periodically compute and cache a new "is
// bookmarked" state, and use that whenever the menu has been opened.
isInPrimaryState = { currentUrlIsBookmarked },
secondaryImageResource = R.drawable.ic_bookmark_outline,
secondaryContentDescription = context.getString(R.string.browser_menu_bookmark),
secondaryImageTintResource = primaryTextColor(),
disableInSecondaryState = false
) {
if (!currentUrlIsBookmarked) currentUrlIsBookmarked = true
onItemTapped.invoke(ToolbarMenu.Item.Bookmark)
}
if (FeatureFlags.toolbarMenuFeature) {
BrowserMenuItemToolbar(listOf(back, forward, share, refresh))
} else {
val bookmark = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = R.drawable.ic_bookmark_filled,
primaryContentDescription = context.getString(R.string.browser_menu_edit_bookmark),
primaryImageTintResource = primaryTextColor(),
// TwoStateButton.isInPrimaryState must be synchronous, and checking bookmark state is
// relatively slow. The best we can do here is periodically compute and cache a new "is
// bookmarked" state, and use that whenever the menu has been opened.
isInPrimaryState = { isCurrentUrlBookmarked },
secondaryImageResource = R.drawable.ic_bookmark_outline,
secondaryContentDescription = context.getString(R.string.browser_menu_bookmark),
secondaryImageTintResource = primaryTextColor(),
disableInSecondaryState = false
) {
if (!isCurrentUrlBookmarked) isCurrentUrlBookmarked = true
onItemTapped.invoke(ToolbarMenu.Item.Bookmark)
}
BrowserMenuItemToolbar(listOf(back, forward, bookmark, share, refresh))
BrowserMenuItemToolbar(listOf(back, forward, bookmark, share, refresh))
}
}
// Predicates that need to be repeatedly called as the session changes
@ -180,7 +185,7 @@ class DefaultToolbarMenu(
appLink(session.content.url).hasExternalApp()
} ?: false
private fun shouldShowReaderAppearance(): Boolean = selectedSession?.let {
private fun shouldShowReaderViewCustomization(): Boolean = selectedSession?.let {
store.state.findTab(it.id)?.readerState?.active
} ?: false
// End of predicates //
@ -287,7 +292,7 @@ class DefaultToolbarMenu(
imageResource = R.drawable.ic_readermode_appearance,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.ReaderModeAppearance)
onItemTapped.invoke(ToolbarMenu.Item.CustomizeReaderView)
}
val openInApp = BrowserMenuHighlightableItem(
@ -349,7 +354,7 @@ class DefaultToolbarMenu(
if (shouldShowSaveToCollection) saveToCollection else null,
desktopMode,
openInApp.apply { visible = ::shouldShowOpenInApp },
readerAppearance.apply { visible = ::shouldShowReaderAppearance },
readerAppearance.apply { visible = ::shouldShowReaderViewCustomization },
BrowserMenuDivider(),
menuToolbar
)
@ -364,8 +369,8 @@ class DefaultToolbarMenu(
private val newCoreMenuItems by lazy {
val newTabItem = BrowserMenuImageText(
context.getString(R.string.library_new_tab),
R.drawable.ic_bookmark_filled,
disabledTextColor()
R.drawable.ic_new,
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.NewTab)
}
@ -373,7 +378,7 @@ class DefaultToolbarMenu(
val bookmarksItem = BrowserMenuImageText(
context.getString(R.string.library_bookmarks),
R.drawable.ic_bookmark_filled,
disabledTextColor()
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Bookmarks)
}
@ -381,7 +386,7 @@ class DefaultToolbarMenu(
val historyItem = BrowserMenuImageText(
context.getString(R.string.library_history),
R.drawable.ic_history,
disabledTextColor()
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.History)
}
@ -389,7 +394,7 @@ class DefaultToolbarMenu(
val downloadsItem = BrowserMenuImageText(
context.getString(R.string.library_downloads),
R.drawable.ic_download,
disabledTextColor()
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Downloads)
}
@ -397,15 +402,15 @@ class DefaultToolbarMenu(
val extensionsItem = BrowserMenuImageText(
context.getString(R.string.browser_menu_extensions),
R.drawable.ic_addons_extensions,
disabledTextColor()
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.AddonsManager)
}
val syncedTabsItem = BrowserMenuImageText(
context.getString(R.string.library_synced_tabs),
R.drawable.ic_synced_tabs,
disabledTextColor()
val syncedTabs = BrowserMenuImageText(
label = context.getString(R.string.synced_tabs),
imageResource = R.drawable.ic_synced_tabs,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs)
}
@ -413,7 +418,7 @@ class DefaultToolbarMenu(
val findInPageItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_find_in_page),
imageResource = R.drawable.mozac_ic_search,
iconTintColorResource = disabledTextColor()
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
}
@ -428,10 +433,35 @@ class DefaultToolbarMenu(
onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked))
}
val customizeReaderView = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_customize_reader_view),
imageResource = R.drawable.ic_readermode_appearance,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.CustomizeReaderView)
}
val openInApp = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_open_app_link),
startImageResource = R.drawable.ic_open_in_app,
iconTintColorResource = primaryTextColor(),
highlight = BrowserMenuHighlight.LowPriority(
label = context.getString(R.string.browser_menu_open_app_link),
notificationTint = getColor(context, R.color.whats_new_notification_color)
),
isHighlighted = { !context.settings().openInAppOpened }
) {
onItemTapped.invoke(ToolbarMenu.Item.OpenInApp)
}
val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem(
id = WebCompatReporterFeature.WEBCOMPAT_REPORTER_EXTENSION_ID
)
val addToHomeScreenItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_add_to_homescreen),
imageResource = R.drawable.ic_add_to_homescreen,
iconTintColorResource = disabledTextColor()
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen)
}
@ -439,7 +469,7 @@ class DefaultToolbarMenu(
val addToTopSitesItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_add_to_top_sites),
imageResource = R.drawable.ic_top_sites,
iconTintColorResource = disabledTextColor()
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.AddToTopSites)
}
@ -447,7 +477,7 @@ class DefaultToolbarMenu(
val saveToCollectionItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_save_to_collection_2),
imageResource = R.drawable.ic_tab_collection,
iconTintColorResource = disabledTextColor()
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection)
}
@ -455,7 +485,7 @@ class DefaultToolbarMenu(
val settingsItem = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_settings),
startImageResource = R.drawable.ic_settings,
iconTintColorResource = disabledTextColor(),
iconTintColorResource = primaryTextColor(),
textColorResource = if (hasAccountProblem)
ThemeManager.resolveAttribute(R.attr.primaryText, context) else
primaryTextColor(),
@ -469,26 +499,31 @@ class DefaultToolbarMenu(
onItemTapped.invoke(ToolbarMenu.Item.Settings)
}
val menuItems = listOfNotNull(
newTabItem,
BrowserMenuDivider(),
bookmarksItem,
historyItem,
downloadsItem,
extensionsItem,
syncedTabsItem,
BrowserMenuDivider(),
findInPageItem,
desktopSiteItem,
BrowserMenuDivider(),
addToHomeScreenItem.apply { visible = ::canAddToHomescreen },
addToTopSitesItem,
saveToCollectionItem,
BrowserMenuDivider(),
settingsItem,
BrowserMenuDivider(),
menuToolbar
)
val menuItems =
listOfNotNull(
if (isTopToolbarSelected) menuToolbar else null,
newTabItem,
BrowserMenuDivider(),
bookmarksItem,
historyItem,
downloadsItem,
extensionsItem,
syncedTabs,
BrowserMenuDivider(),
findInPageItem,
desktopSiteItem,
customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization },
openInApp.apply { visible = ::shouldShowOpenInApp },
reportSiteIssuePlaceholder,
BrowserMenuDivider(),
addToHomeScreenItem.apply { visible = ::canAddToHomescreen },
addToTopSitesItem,
saveToCollectionItem,
BrowserMenuDivider(),
settingsItem,
if (isTopToolbarSelected) null else BrowserMenuDivider(),
if (isTopToolbarSelected) null else menuToolbar
)
menuItems
}
@ -497,10 +532,6 @@ class DefaultToolbarMenu(
@VisibleForTesting
internal fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context)
@ColorRes
@VisibleForTesting
internal fun disabledTextColor() = R.color.toolbar_menu_transparent
@VisibleForTesting
internal fun registerForIsBookmarkedUpdates() {
store.flowScoped(lifecycleOwner) { flow ->
@ -512,7 +543,7 @@ class DefaultToolbarMenu(
)
}
.collect {
currentUrlIsBookmarked = false
isCurrentUrlBookmarked = false
updateCurrentUrlIsBookmarked(it.content.url)
}
}
@ -522,7 +553,7 @@ class DefaultToolbarMenu(
internal fun updateCurrentUrlIsBookmarked(newUrl: String) {
isBookmarkedJob?.cancel()
isBookmarkedJob = lifecycleOwner.lifecycleScope.launch {
currentUrlIsBookmarked = bookmarksStorage
isCurrentUrlBookmarked = bookmarksStorage
.getBookmarksWithUrl(newUrl)
.any { it.url == newUrl }
}

@ -19,6 +19,7 @@ import mozilla.components.concept.engine.Engine
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.tabs.toolbar.TabCounterToolbarButton
import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature
import mozilla.components.feature.toolbar.ToolbarBehaviorController
import mozilla.components.feature.toolbar.ToolbarFeature
import mozilla.components.feature.toolbar.ToolbarPresenter
import mozilla.components.support.base.feature.LifecycleAwareFeature
@ -53,6 +54,8 @@ abstract class ToolbarIntegration(
private val menuPresenter =
MenuPresenter(toolbar, context.components.core.store, sessionId)
private val toolbarController = ToolbarBehaviorController(toolbar, store, sessionId)
init {
toolbar.display.menuBuilder = toolbarMenu.menuBuilder
toolbar.private = isPrivate
@ -61,11 +64,13 @@ abstract class ToolbarIntegration(
override fun start() {
menuPresenter.start()
toolbarPresenter.start()
toolbarController.start()
}
override fun stop() {
menuPresenter.stop()
toolbarPresenter.stop()
toolbarController.stop()
}
fun invalidateMenu() {

@ -27,7 +27,7 @@ interface ToolbarMenu {
object Quit : Item()
object OpenInApp : Item()
object Bookmark : Item()
object ReaderModeAppearance : Item()
object CustomizeReaderView : Item()
object Bookmarks : Item()
object History : Item()
object Downloads : Item()

@ -9,8 +9,6 @@ import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.CustomTabConfig
import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.EngineSession
@ -22,7 +20,7 @@ import mozilla.components.feature.intent.processing.IntentProcessor
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.feature.tabs.TabsUseCases
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent
@ -38,8 +36,7 @@ import java.io.IOException
*/
class FennecWebAppIntentProcessor(
private val context: Context,
private val sessionManager: SessionManager,
private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase,
private val addNewTabUseCase: TabsUseCases.AddNewTabUseCase,
private val storage: ManifestStorage
) : IntentProcessor {
val logger = Logger("FennecWebAppIntentProcessor")
@ -62,16 +59,14 @@ class FennecWebAppIntentProcessor(
return if (!url.isNullOrEmpty() && matches(intent)) {
val webAppManifest = runBlockingIncrement { loadManifest(safeIntent, url) }
val session = Session(url, private = false, source = SessionState.Source.HOME_SCREEN)
session.webAppManifest = webAppManifest
session.customTabConfig =
webAppManifest?.toCustomTabConfig() ?: createFallbackCustomTabConfig()
sessionManager.add(session)
loadUrlUseCase(url, session.id, EngineSession.LoadUrlFlags.external())
intent.putSessionId(session.id)
val sessionId = addNewTabUseCase(
url = url,
source = SessionState.Source.HOME_SCREEN,
flags = EngineSession.LoadUrlFlags.external(),
webAppManifest = webAppManifest,
customTabConfig = webAppManifest?.toCustomTabConfig() ?: createFallbackCustomTabConfig()
)
intent.putSessionId(sessionId)
if (webAppManifest != null) {
intent.flags = FLAG_ACTIVITY_NEW_DOCUMENT

@ -4,8 +4,10 @@
package org.mozilla.fenix.downloads
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.webkit.MimeTypeMap
import androidx.coordinatorlayout.widget.CoordinatorLayout
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.download_dialog_layout.view.*
@ -30,7 +32,7 @@ class DynamicDownloadDialog(
private val metrics: MetricController,
private val didFail: Boolean,
private val tryAgain: (String) -> Unit,
private val onCannotOpenFile: () -> Unit,
private val onCannotOpenFile: (DownloadState) -> Unit,
private val view: View,
private val toolbarHeight: Int,
private val onDismiss: () -> Unit
@ -110,7 +112,7 @@ class DynamicDownloadDialog(
)
if (!fileWasOpened) {
onCannotOpenFile()
onCannotOpenFile(downloadState)
}
context.metrics.track(Event.InAppNotificationDownloadOpen)
@ -138,4 +140,15 @@ class DynamicDownloadDialog(
view.visibility = View.GONE
onDismiss()
}
companion object {
fun getCannotOpenFileErrorMessage(context: Context, download: DownloadState): String {
val fileExt = MimeTypeMap.getFileExtensionFromUrl(
download.filePath
)
return context.getString(
R.string.mozac_feature_downloads_open_not_supported1, fileExt
)
}
}
}

@ -10,9 +10,11 @@ import android.os.StrictMode
import io.sentry.Sentry
import mozilla.components.service.nimbus.NimbusApi
import mozilla.components.service.nimbus.Nimbus
import mozilla.components.service.nimbus.NimbusAppInfo
import mozilla.components.service.nimbus.NimbusDisabled
import mozilla.components.service.nimbus.NimbusServerSettings
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.components.isSentryEnabled
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
@ -37,7 +39,20 @@ fun createNimbus(context: Context, url: String?): NimbusApi =
context.settings().isExperimentationEnabled
}
Nimbus(context, serverSettings).apply {
// The name "fenix" here corresponds to the app_name defined for the family of apps
// that encompasses all of the channels for the Fenix app. This is defined upstream in
// the telemetry system. For more context on where the app_name come from see:
// https://probeinfo.telemetry.mozilla.org/v2/glean/app-listings
// and
// https://github.com/mozilla/probe-scraper/blob/master/repositories.yaml
val appInfo = NimbusAppInfo(
appName = "fenix",
// Note: Using BuildConfig.BUILD_TYPE is important here so that it matches the value
// passed into Glean. `Config.channel.toString()` turned out to be non-deterministic
// and would mostly produce the value `Beta` and rarely would produce `beta`.
channel = BuildConfig.BUILD_TYPE
)
Nimbus(context, appInfo, serverSettings).apply {
// This performs the minimal amount of work required to load branch and enrolment data
// into memory. If `getExperimentBranch` is called from another thread between here
// and the next nimbus disk write (setting `globalUserParticipation` or

@ -24,6 +24,19 @@ fun Activity.enterToImmersiveMode() {
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
}
/**
* Attempts to come out from immersive mode using the View.
*/
fun Activity.exitImmersiveModeIfNeeded() {
if (WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON and window.attributes.flags == 0) {
// We left immersive mode already.
return
}
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
}
fun Activity.breadcrumb(
message: String,
data: Map<String, String> = emptyMap()

@ -0,0 +1,31 @@
/* 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.ext
import mozilla.telemetry.glean.private.TimingDistributionMetricType
/**
* A reimplementation of [TimingDistributionMetricType.measure] that address unintuitive
* issues around non-local returns: see https://bugzilla.mozilla.org/show_bug.cgi?id=1699505.
* This should be removed once that bug is resolved. That method's kdoc is as follows:
*
* Convenience method to simplify measuring a function or block of code.
*
* If the measured function throws, the measurement is canceled and the exception rethrown.
*/
@Suppress("TooGenericExceptionCaught")
fun <U> TimingDistributionMetricType.measureNoInline(funcToMeasure: () -> U): U {
val timerId = start()
val returnValue = try {
funcToMeasure()
} catch (e: Exception) {
cancel(timerId)
throw e
}
stopAndAccumulate(timerId)
return returnValue
}

@ -85,6 +85,7 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.ui.tabcounter.TabCounterMenu
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.Config
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
@ -101,6 +102,7 @@ import org.mozilla.fenix.components.toolbar.FenixTabCounterMenu
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
@ -193,7 +195,7 @@ class HomeFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View? = PerfStartup.homeFragmentOnCreateView.measureNoInline {
val view = inflater.inflate(R.layout.fragment_home, container, false)
val activity = activity as HomeActivity
val components = requireComponents
@ -274,7 +276,7 @@ class HomeFragment : Fragment() {
appBarLayout = view.homeAppBar
activity.themeManager.applyStatusBarTheme(activity)
return view
view
}
override fun onConfigurationChanged(newConfig: Configuration) {
@ -356,7 +358,8 @@ class HomeFragment : Fragment() {
}
@Suppress("LongMethod", "ComplexMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) =
PerfStartup.homeFragmentOnViewCreated.measureNoInline { // weird indent so we don't have to break blame.
super.onViewCreated(view, savedInstanceState)
observeSearchEngineChanges()
@ -1013,9 +1016,14 @@ class HomeFragment : Fragment() {
}
private fun openTabTray() {
val direction = if (requireContext().settings().tabsTrayRewrite) {
HomeFragmentDirections.actionGlobalTabsTrayFragment()
} else {
HomeFragmentDirections.actionGlobalTabTrayDialogFragment()
}
findNavController().nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalTabTrayDialogFragment()
direction
)
}

@ -7,13 +7,11 @@ package org.mozilla.fenix.home.intent
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.EngineSession
import mozilla.components.feature.intent.ext.putSessionId
import mozilla.components.feature.intent.processing.IntentProcessor
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.support.utils.toSafeIntent
/**
@ -21,8 +19,7 @@ import mozilla.components.support.utils.toSafeIntent
* https://developer.android.com/guide/topics/ui/shortcuts/creating-shortcuts#pinned
*/
class FennecBookmarkShortcutsIntentProcessor(
private val sessionManager: SessionManager,
private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase
private val addNewTabUseCase: TabsUseCases.AddNewTabUseCase
) : IntentProcessor {
/**
@ -41,13 +38,15 @@ class FennecBookmarkShortcutsIntentProcessor(
val url = safeIntent.dataString
return if (!url.isNullOrEmpty() && matches(intent)) {
val session = Session(url, private = false, source = SessionState.Source.HOME_SCREEN)
sessionManager.add(session, selected = true)
loadUrlUseCase(url, session.id, EngineSession.LoadUrlFlags.external())
val sessionId = addNewTabUseCase(
url = url,
flags = EngineSession.LoadUrlFlags.external(),
source = SessionState.Source.HOME_SCREEN,
selectTab = true,
startLoading = true
)
intent.action = ACTION_VIEW
intent.putSessionId(session.id)
intent.putSessionId(sessionId)
true
} else {
false

@ -13,6 +13,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.availableSearchEngines
import mozilla.components.browser.state.state.searchEngines
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore
@ -325,7 +326,11 @@ class DefaultSessionControlController(
setPositiveButton(R.string.top_sites_rename_dialog_ok) { dialog, _ ->
viewLifecycleScope.launch(Dispatchers.IO) {
with(activity.components.useCases.topSitesUseCase) {
renameTopSites(topSite, topSiteLabelEditText.text.toString())
updateTopSites(
topSite,
topSiteLabelEditText.text.toString(),
topSite.url
)
}
}
dialog.dismiss()
@ -380,19 +385,18 @@ class DefaultSessionControlController(
metrics.track(Event.PocketTopSiteClicked)
}
if (SupportUtils.GOOGLE_URL.equals(url, true)) {
val availableEngines = getAvailableSearchEngines()
val availableEngines = getAvailableSearchEngines()
val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.TOPSITE
val event =
availableEngines.firstOrNull { engine -> engine.suggestUrl?.contains(url) == true }
?.let { searchEngine ->
searchAccessPoint.let { sap ->
MetricsUtils.createSearchEvent(searchEngine, store, sap)
}
}
event?.let { activity.metrics.track(it) }
}
val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.TOPSITE
val event =
availableEngines.firstOrNull {
engine -> engine.resultUrls.firstOrNull { it.contains(url) } != null
}?.let {
searchEngine -> searchAccessPoint.let { sap ->
MetricsUtils.createSearchEvent(searchEngine, store, sap)
}
}
event?.let { activity.metrics.track(it) }
addTabUseCase.invoke(
url = appendSearchAttributionToUrlIfNeeded(url),
@ -403,13 +407,9 @@ class DefaultSessionControlController(
}
@VisibleForTesting
internal fun getAvailableSearchEngines() = activity
.components
.core
.store
.state
.search
.searchEngines
internal fun getAvailableSearchEngines() =
activity.components.core.store.state.search.searchEngines +
activity.components.core.store.state.search.availableSearchEngines
/**
* Append a search attribution query to any provided search engine URL based on the

@ -11,6 +11,7 @@ import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.R
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSitesAdapter
import org.mozilla.fenix.utils.AccessibilityGridLayoutManager
class TopSiteViewHolder(
view: View,
@ -20,8 +21,12 @@ class TopSiteViewHolder(
private val topSitesAdapter = TopSitesAdapter(interactor)
init {
val gridLayoutManager =
AccessibilityGridLayoutManager(view.context, SPAN_COUNT)
view.top_sites_list.apply {
adapter = topSitesAdapter
layoutManager = gridLayoutManager
}
}
@ -31,5 +36,6 @@ class TopSiteViewHolder(
companion object {
const val LAYOUT_ID = R.layout.component_top_sites
const val SPAN_COUNT = 4
}
}

@ -188,7 +188,7 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
}
R.id.open_history_in_new_tabs_multi_select -> {
openItemsInNewTab { selectedItem ->
requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
requireComponents.analytics.metrics.track(Event.HistoryOpenedInNewTabs)
selectedItem.url
}
@ -197,7 +197,7 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
}
R.id.open_history_in_private_tabs_multi_select -> {
openItemsInNewTab(private = true) { selectedItem ->
requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
requireComponents.analytics.metrics.track(Event.HistoryOpenedInPrivateTabs)
selectedItem.url
}
@ -248,7 +248,11 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
}
private fun openItem(item: HistoryItem, mode: BrowsingMode? = null) {
requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
when (mode?.isPrivate) {
true -> requireComponents.analytics.metrics.track(Event.HistoryOpenedInPrivateTab)
false -> requireComponents.analytics.metrics.track(Event.HistoryOpenedInNewTab)
null -> requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
}
mode?.let { (activity as HomeActivity).browsingModeManager.mode = it }

@ -57,7 +57,7 @@ internal class StartupFrameworkStartMeasurement(
val clockTicksPerSecond = stat.clockTicksPerSecond.also {
// framework* is derived from the number of clock ticks per second. To ensure this
// value does not throw off our result, we capture it too.
telemetry.clockTicksPerSecond.add(it.toInt())
telemetry.clockTicksPerSecondV2.set(it)
}
// In our brief analysis, clock ticks per second was overwhelmingly equal to 100. To make

@ -42,6 +42,7 @@ interface SearchController {
fun handleExistingSessionSelected(tabId: String)
fun handleSearchShortcutsButtonClicked()
fun handleCameraPermissionsNeeded()
fun handleSearchEngineSuggestionClicked(searchEngine: SearchEngine)
}
@Suppress("TooManyFunctions", "LongParameterList")
@ -55,7 +56,8 @@ class SearchDialogController(
private val metrics: MetricController,
private val dismissDialog: () -> Unit,
private val clearToolbarFocus: () -> Unit,
private val focusToolbar: () -> Unit
private val focusToolbar: () -> Unit,
private val clearToolbar: () -> Unit
) : SearchController {
override fun handleUrlCommitted(url: String) {
@ -221,6 +223,11 @@ class SearchDialogController(
dialog.show()
}
override fun handleSearchEngineSuggestionClicked(searchEngine: SearchEngine) {
clearToolbar()
handleSearchShortcutEngineSelected(searchEngine)
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun buildDialog(): AlertDialog.Builder {
return AlertDialog.Builder(activity).apply {

@ -100,6 +100,8 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
super.onStop()
// https://github.com/mozilla-mobile/fenix/issues/14279
// Let's reset back to the default behavior after we're done searching
// This will be addressed on https://github.com/mozilla-mobile/fenix/issues/17805
@Suppress("DEPRECATION")
requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
}
@ -157,7 +159,12 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
toolbarView.view.hideKeyboard()
toolbarView.view.clearFocus()
},
focusToolbar = { toolbarView.view.edit.focus() }
focusToolbar = { toolbarView.view.edit.focus() },
clearToolbar = {
toolbarView.view
.findViewById<InlineAutocompleteEditText>(R.id.mozac_browser_toolbar_edit_url_view)
?.setText("")
}
)
)

@ -37,6 +37,10 @@ class SearchDialogInteractor(
searchController.handleSearchTermsTapped(searchTerms)
}
override fun onSearchEngineSuggestionSelected(searchEngine: SearchEngine) {
searchController.handleSearchEngineSuggestionClicked(searchEngine)
}
override fun onSearchShortcutEngineSelected(searchEngine: SearchEngine) {
searchController.handleSearchShortcutEngineSelected(searchEngine)
}

@ -44,4 +44,9 @@ interface AwesomeBarInteractor {
* Called whenever the Shortcuts button is clicked
*/
fun onSearchShortcutsButtonClicked()
/**
* Called whenever search engine suggestion is tapped
*/
fun onSearchEngineSuggestionSelected(searchEngine: SearchEngine)
}

@ -11,11 +11,13 @@ import androidx.core.graphics.drawable.toBitmap
import mozilla.components.browser.awesomebar.BrowserAwesomeBar
import mozilla.components.browser.search.DefaultSearchEngineProvider
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.state.searchEngines
import mozilla.components.concept.awesomebar.AwesomeBar
import mozilla.components.concept.engine.EngineSession
import mozilla.components.feature.awesomebar.provider.BookmarksStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SearchActionProvider
import mozilla.components.feature.awesomebar.provider.SearchEngineSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
import mozilla.components.feature.search.SearchUseCases
@ -51,6 +53,7 @@ class AwesomeBarView(
private val syncedTabsStorageSuggestionProvider: SyncedTabsStorageSuggestionProvider
private val defaultSearchSuggestionProvider: SearchSuggestionProvider
private val defaultSearchActionProvider: SearchActionProvider
private val searchEngineSuggestionProvider: SearchEngineSuggestionProvider
private val searchSuggestionProviderMap: MutableMap<SearchEngine, List<AwesomeBar.SuggestionProvider>>
private var providersInUse = mutableSetOf<AwesomeBar.SuggestionProvider>()
@ -143,6 +146,8 @@ class AwesomeBarView(
colorFilter = createBlendModeColorFilterCompat(primaryTextColor, SRC_IN)
}.toBitmap()
val searchWithBitmap = getDrawable(activity, R.drawable.ic_search_with)?.toBitmap()
defaultSearchSuggestionProvider =
SearchSuggestionProvider(
context = activity,
@ -173,6 +178,16 @@ class AwesomeBarView(
selectShortcutEngineSettings = interactor::onClickSearchEngineSettings
)
searchEngineSuggestionProvider =
SearchEngineSuggestionProvider(
context = activity,
searchEnginesList = components.core.store.state.search.searchEngines,
selectShortcutEngine = interactor::onSearchEngineSuggestionSelected,
title = R.string.search_engine_suggestions_title,
description = activity.getString(R.string.search_engine_suggestions_description),
searchIcon = searchWithBitmap
)
searchSuggestionProviderMap = HashMap()
}
@ -242,6 +257,8 @@ class AwesomeBarView(
providersToAdd.add(sessionProvider)
}
providersToAdd.add(searchEngineSuggestionProvider)
return providersToAdd
}

@ -39,7 +39,7 @@ data class SearchProviderModel(
* Checks if any of the given URLs represent an ad from the search engine.
* Used to check if a clicked link was for an ad.
*/
fun containsAds(urlList: List<String>) = urlList.any { url -> isAd(url) }
fun containsAdLinks(urlList: List<String>) = urlList.any { url -> isAd(url) }
private fun isAd(url: String) =
extraAdServersRegexps.any { adsRegex -> adsRegex.containsMatchIn(url) }

@ -0,0 +1,36 @@
/* 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.search.telemetry
import java.util.Locale
/**
* A data class that tracks key information about a Search Engine Result Page (SERP).
*
* @property provider The name of the search provider.
* @property type The search access point type (SAP). This is either "organic", "sap" or
* "sap-follow-on".
* @property code The search URL's `code` query parameter.
* @property channel The search URL's `channel` query parameter.
*/
internal data class TrackKeyInfo(
var provider: String,
var type: String,
var code: String?,
var channel: String? = null
) {
/**
* Returns the track key information into the following string format:
* `<provider>.in-content.[sap|sap-follow-on|organic].[code|none](.[channel])?`.
*/
fun createTrackKey(): String {
return "${provider.toLowerCase(Locale.ROOT)}.in-content" +
".${type.toLowerCase(Locale.ROOT)}" +
".${code?.toLowerCase(Locale.ROOT) ?: "none"}" +
if (!channel?.toLowerCase(Locale.ROOT).isNullOrBlank())
".${channel?.toLowerCase(Locale.ROOT)}"
else ""
}
}

@ -0,0 +1,96 @@
/* 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.search.telemetry
import android.net.Uri
import org.json.JSONObject
private const val SEARCH_TYPE_SAP_FOLLOW_ON = "sap-follow-on"
private const val SEARCH_TYPE_SAP = "sap"
private const val SEARCH_TYPE_ORGANIC = "organic"
private const val CHANNEL_KEY = "channel"
internal fun getTrackKey(
provider: SearchProviderModel,
uri: Uri,
cookies: List<JSONObject>
): String {
val paramSet = uri.queryParameterNames
var code: String? = null
if (provider.codeParam.isNotEmpty()) {
code = uri.getQueryParameter(provider.codeParam)
// Try cookies first because Bing has followOnCookies and valid code, but no
// followOnParams => would tracks organic instead of sap-follow-on
if (provider.followOnCookies.isNotEmpty()) {
// Checks if engine contains a valid follow-on cookie, otherwise return default
getTrackKeyFromCookies(provider, uri, cookies)?.let {
return it.createTrackKey()
}
}
// For Bing if it didn't have a valid cookie and for all the other search engines
if (hasValidCode(code, provider)) {
val channel = uri.getQueryParameter(CHANNEL_KEY)
val type = getSapType(provider.followOnParams, paramSet)
return TrackKeyInfo(provider.name, type, code, channel).createTrackKey()
}
}
// Default to organic search type if no code parameter was found.
return TrackKeyInfo(provider.name, SEARCH_TYPE_ORGANIC, code).createTrackKey()
}
private fun getTrackKeyFromCookies(
provider: SearchProviderModel,
uri: Uri,
cookies: List<JSONObject>
): TrackKeyInfo? {
// Especially Bing requires lots of extra work related to cookies.
for (followOnCookie in provider.followOnCookies) {
val eCode = uri.getQueryParameter(followOnCookie.extraCodeParam)
if (eCode == null || !followOnCookie.extraCodePrefixes.any { prefix ->
eCode.startsWith(prefix)
}) {
continue
}
// If this cookie is present, it's probably an SAP follow-on.
// This might be an organic follow-on in the same session, but there
// is no way to tell the difference.
for (cookie in cookies) {
if (cookie.getString("name") != followOnCookie.name) {
continue
}
val valueList = cookie.getString("value")
.split("=")
.map { item -> item.trim() }
if (valueList.size == 2 && valueList[0] == followOnCookie.codeParam &&
followOnCookie.codePrefixes.any { prefix ->
valueList[1].startsWith(
prefix
)
}
) {
return TrackKeyInfo(provider.name, SEARCH_TYPE_SAP_FOLLOW_ON, valueList[1])
}
}
}
return null
}
private fun getSapType(followOnParams: List<String>, paramSet: Set<String>): String {
return if (followOnParams.any { param -> paramSet.contains(param) }) {
SEARCH_TYPE_SAP_FOLLOW_ON
} else {
SEARCH_TYPE_SAP
}
}
private fun hasValidCode(code: String?, provider: SearchProviderModel): Boolean =
code != null && provider.codePrefixes.any { prefix -> code.startsWith(prefix) }

@ -5,6 +5,7 @@
package org.mozilla.fenix.search.telemetry.ads
import androidx.annotation.VisibleForTesting
import androidx.core.net.toUri
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import org.json.JSONObject
@ -12,9 +13,14 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.search.telemetry.BaseSearchTelemetry
import org.mozilla.fenix.search.telemetry.ExtensionInfo
import org.mozilla.fenix.search.telemetry.getTrackKey
class AdsTelemetry(private val metrics: MetricController) : BaseSearchTelemetry() {
// Cache the cookies provided by the ADS_EXTENSION_ID extension to be used when tracking
// the Ads clicked telemetry.
var cachedCookies = listOf<JSONObject>()
override fun install(
engine: Engine,
store: BrowserStore
@ -27,38 +33,64 @@ class AdsTelemetry(private val metrics: MetricController) : BaseSearchTelemetry(
installWebExtension(engine, store, info)
}
fun trackAdClickedMetric(sessionUrl: String?, urlPath: List<String>) {
if (sessionUrl == null) {
return
}
val provider = getProviderForUrl(sessionUrl)
provider?.let {
if (it.containsAds(urlPath)) {
metrics.track(Event.SearchAdClicked(it.name))
}
}
}
override fun processMessage(message: JSONObject) {
// Cache the cookies list when the extension sends a message.
cachedCookies = getMessageList(
message,
ADS_MESSAGE_COOKIES_KEY
)
val urls = getMessageList<String>(message, ADS_MESSAGE_DOCUMENT_URLS_KEY)
val provider = getProviderForUrl(message.getString(ADS_MESSAGE_SESSION_URL_KEY))
provider?.let {
if (it.containsAds(urls)) {
if (it.containsAdLinks(urls)) {
metrics.track(Event.SearchWithAds(it.name))
}
}
}
/**
* If a search ad is clicked, record the search ad that was clicked. This method is called
* when the browser is navigating to a new URL, which may be a search ad.
*
* @param url The URL of the page before the search ad was clicked. This is used to determine
* the originating search provider.
* @param urlPath A list of the URLs and load requests collected in between location changes.
* Clicking on a search ad generates a list of redirects from the originating search provider
* to the ad source. This is used to determine if there was an ad click.
*/
fun trackAdClickedMetric(url: String?, urlPath: List<String>) {
val uri = url?.toUri() ?: return
val provider = getProviderForUrl(url) ?: return
val paramSet = uri.queryParameterNames
if (!paramSet.contains(provider.queryParam) || !provider.containsAdLinks(urlPath)) {
// Do nothing if the URL does not have the search provider's query parameter or
// there were no ad clicks.
return
}
metrics.track(Event.SearchAdClicked(getTrackKey(provider, uri, cachedCookies)))
}
companion object {
@VisibleForTesting
internal const val ADS_EXTENSION_ID = "ads@mozac.org"
@VisibleForTesting
internal const val ADS_EXTENSION_RESOURCE_URL = "resource://android/assets/extensions/ads/"
@VisibleForTesting
internal const val ADS_MESSAGE_SESSION_URL_KEY = "url"
@VisibleForTesting
internal const val ADS_MESSAGE_DOCUMENT_URLS_KEY = "urls"
@VisibleForTesting
internal const val ADS_MESSAGE_ID = "MozacBrowserAds"
@VisibleForTesting
internal const val ADS_MESSAGE_COOKIES_KEY = "cookies"
}
}

@ -4,7 +4,6 @@
package org.mozilla.fenix.search.telemetry.incontent
import android.net.Uri
import androidx.annotation.VisibleForTesting
import androidx.core.net.toUri
import mozilla.components.browser.state.store.BrowserStore
@ -14,7 +13,7 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.search.telemetry.BaseSearchTelemetry
import org.mozilla.fenix.search.telemetry.ExtensionInfo
import org.mozilla.fenix.search.telemetry.SearchProviderModel
import org.mozilla.fenix.search.telemetry.getTrackKey
class InContentTelemetry(private val metrics: MetricController) : BaseSearchTelemetry() {
@ -37,116 +36,32 @@ class InContentTelemetry(private val metrics: MetricController) : BaseSearchTele
@VisibleForTesting
internal fun trackPartnerUrlTypeMetric(url: String, cookies: List<JSONObject>) {
val provider = getProviderForUrl(url)
var trackKey: TrackKeyInfo? = null
val provider = getProviderForUrl(url) ?: return
val uri = url.toUri()
val paramSet = uri.queryParameterNames
provider?.let {
val uri = url.toUri()
val paramSet = uri.queryParameterNames
if (!paramSet.contains(provider.queryParam)) {
return
}
var code: String? = null
if (provider.codeParam.isNotEmpty()) {
code = uri.getQueryParameter(provider.codeParam)
// Try cookies first because Bing has followOnCookies and valid code, but no
// followOnParams => would tracks organic instead of sap-follow-on
if (provider.followOnCookies.isNotEmpty()) {
// Checks if engine contains a valid follow-on cookie, otherwise return default
trackKey = getTrackKeyFromCookies(provider, uri, cookies, code)
}
// For Bing if it didn't have a valid cookie and for all the other search engines
if (resultNotComputedFromCookies(trackKey) && hasValidCode(code, provider)) {
val channel = uri.getQueryParameter(CHANNEL_KEY)
val type = getSapType(provider.followOnParams, paramSet)
trackKey = TrackKeyInfo(provider.name, type, code, channel)
}
}
// Go default if no codeParam was found
if (trackKey == null) {
trackKey = TrackKeyInfo(provider.name, SEARCH_TYPE_ORGANIC, code)
}
trackKey?.let {
metrics.track(Event.SearchInContent(it.createTrackKey()))
}
if (!paramSet.contains(provider.queryParam)) {
return
}
}
private fun resultNotComputedFromCookies(trackKey: TrackKeyInfo?): Boolean =
trackKey == null || trackKey.type == SEARCH_TYPE_ORGANIC
private fun hasValidCode(code: String?, provider: SearchProviderModel): Boolean =
code != null && provider.codePrefixes.any { prefix -> code.startsWith(prefix) }
private fun getSapType(followOnParams: List<String>, paramSet: Set<String>): String {
return if (followOnParams.any { param -> paramSet.contains(param) }) {
SEARCH_TYPE_SAP_FOLLOW_ON
} else {
SEARCH_TYPE_SAP
}
}
private fun getTrackKeyFromCookies(
provider: SearchProviderModel,
uri: Uri,
cookies: List<JSONObject>,
code: String?
): TrackKeyInfo {
// Especially Bing requires lots of extra work related to cookies.
for (followOnCookie in provider.followOnCookies) {
val eCode = uri.getQueryParameter(followOnCookie.extraCodeParam)
if (eCode == null || !followOnCookie.extraCodePrefixes.any { prefix ->
eCode.startsWith(prefix)
}) {
continue
}
// If this cookie is present, it's probably an SAP follow-on.
// This might be an organic follow-on in the same session, but there
// is no way to tell the difference.
for (cookie in cookies) {
if (cookie.getString("name") != followOnCookie.name) {
continue
}
val valueList = cookie.getString("value")
.split("=")
.map { item -> item.trim() }
if (valueList.size == 2 && valueList[0] == followOnCookie.codeParam &&
followOnCookie.codePrefixes.any { prefix ->
valueList[1].startsWith(
prefix
)
}
) {
return TrackKeyInfo(provider.name, SEARCH_TYPE_SAP_FOLLOW_ON, valueList[1])
}
}
}
return TrackKeyInfo(provider.name, SEARCH_TYPE_ORGANIC, code)
metrics.track(Event.SearchInContent(getTrackKey(provider, uri, cookies)))
}
companion object {
@VisibleForTesting
internal const val COOKIES_EXTENSION_ID = "cookies@mozac.org"
@VisibleForTesting
internal const val COOKIES_EXTENSION_RESOURCE_URL =
"resource://android/assets/extensions/cookies/"
@VisibleForTesting
internal const val COOKIES_MESSAGE_SESSION_URL_KEY = "url"
@VisibleForTesting
internal const val COOKIES_MESSAGE_LIST_KEY = "cookies"
@VisibleForTesting
internal const val COOKIES_MESSAGE_ID = "BrowserCookiesMessage"
private const val SEARCH_TYPE_ORGANIC = "organic"
private const val SEARCH_TYPE_SAP = "sap"
private const val SEARCH_TYPE_SAP_FOLLOW_ON = "sap-follow-on"
private const val CHANNEL_KEY = "channel"
}
}

@ -1,23 +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.search.telemetry.incontent
import java.util.Locale
internal data class TrackKeyInfo(
var providerName: String,
var type: String,
var code: String?,
var channel: String? = null
) {
fun createTrackKey(): String {
return "${providerName.toLowerCase(Locale.ROOT)}.in-content" +
".${type.toLowerCase(Locale.ROOT)}" +
".${code?.toLowerCase(Locale.ROOT) ?: "none"}" +
if (!channel?.toLowerCase(Locale.ROOT).isNullOrBlank())
".${channel?.toLowerCase(Locale.ROOT)}"
else ""
}
}

@ -16,6 +16,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import java.util.Locale
/**
* Manages notifications for private tabs.
@ -33,9 +34,28 @@ class PrivateNotificationService : AbstractPrivateNotificationService() {
override fun NotificationCompat.Builder.buildNotification() {
setSmallIcon(R.drawable.ic_private_browsing)
setContentTitle(applicationContext.getString(R.string.app_name_private_4, getString(R.string.app_name)))
setContentText(applicationContext.getString(R.string.notification_pbm_delete_text_2))
color = ContextCompat.getColor(this@PrivateNotificationService, R.color.pbm_notification_color)
setContentTitle(
applicationContext.getString(
R.string.app_name_private_4,
getString(R.string.app_name)
)
)
setContentText(
applicationContext.getString(
R.string.notification_pbm_delete_text_2
)
)
color = ContextCompat.getColor(
this@PrivateNotificationService,
R.color.pbm_notification_color
)
}
/**
* Update the existing notification when the [Locale] has been changed.
*/
override fun notifyLocaleChanged() {
super.refreshNotification()
}
@SuppressLint("MissingSuperCall")

@ -6,7 +6,10 @@ package org.mozilla.fenix.settings
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
class SecretSettingsFragment : PreferenceFragmentCompat() {
@ -18,5 +21,23 @@ class SecretSettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.secret_settings_preferences, rootKey)
requirePreference<SwitchPreference>(R.string.pref_key_show_address_feature).apply {
isVisible = FeatureFlags.addressesFeature
isChecked = context.settings().addressFeature
onPreferenceChangeListener = SharedPreferenceUpdater()
}
requirePreference<SwitchPreference>(R.string.pref_key_show_credit_cards_feature).apply {
isVisible = FeatureFlags.creditCardsFeature
isChecked = context.settings().creditCardsFeature
onPreferenceChangeListener = SharedPreferenceUpdater()
}
requirePreference<SwitchPreference>(R.string.pref_key_new_tabs_tray).apply {
isVisible = FeatureFlags.tabsTrayRewrite
isChecked = context.settings().tabsTrayRewrite
onPreferenceChangeListener = SharedPreferenceUpdater()
}
}
}

@ -40,6 +40,7 @@ import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.application
import org.mozilla.fenix.ext.components
@ -132,7 +133,12 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
val preferencesId = if (FeatureFlags.newIconSet) {
R.xml.preferences_without_icons
} else {
R.xml.preferences
}
setPreferencesFromResource(preferencesId, rootKey)
updateMakeDefaultBrowserPreference()
}
@ -368,8 +374,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
val preferenceOpenLinksInExternalApp =
findPreference<Preference>(getPreferenceKey(R.string.pref_key_open_links_in_external_app))
preferencePrivateBrowsing.icon.mutate().apply {
setTint(requireContext().getColorFromAttr(R.attr.primaryText))
if (!FeatureFlags.newIconSet) {
preferencePrivateBrowsing.icon.mutate().apply {
setTint(requireContext().getColorFromAttr(R.attr.primaryText))
}
}
if (!Config.channel.isReleased) {

@ -6,6 +6,7 @@ package org.mozilla.fenix.settings
import android.content.Context
import android.content.Intent
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import mozilla.components.support.ktx.android.content.appVersionName
@ -99,7 +100,9 @@ object SupportUtils {
fun createCustomTabIntent(context: Context, url: String): Intent = CustomTabsIntent.Builder()
.setInstantAppsEnabled(false)
.setToolbarColor(context.getColorFromAttr(R.attr.foundation))
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder().setToolbarColor(context.getColorFromAttr(R.attr.foundation)).build()
)
.build()
.intent
.setData(url.toUri())

@ -2,10 +2,9 @@
* 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.settings.logins
package org.mozilla.fenix.settings
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import androidx.preference.Preference
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
@ -16,16 +15,33 @@ import mozilla.components.service.fxa.SyncEngine
import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.service.fxa.manager.SyncEnginesStorage
import org.mozilla.fenix.R
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections
/**
* Helper to manage the [R.string.pref_key_password_sync_logins] preference.
* A view to help manage the sync preference in the "Logins and passwords" and "Credit cards"
* settings. The provided [syncPreference] is used to navigate to the different fragments
* that manages the sync account authentication. A summary status will be also added
* depending on the sync account status.
*
* @param syncPreference The sync [Preference] to update and handle navigation.
* @param lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs.
* @param accountManager An instance of [FxaAccountManager].
* @param syncEngine The sync engine that will be used for the sync status lookup.
* @param onSignInToSyncClicked A callback executed when the [syncPreference] is clicked with a
* preference status of "Sign in to Sync".
* @param onSyncStatusClicked A callback executed when the [syncPreference] is clicked with a
* preference status of "On" or "Off".
* @param onReconnectClicked A callback executed when the [syncPreference] is clicked with a
* preference status of "Reconnect".
*/
class SyncLoginsPreferenceView(
private val syncLoginsPreference: Preference,
@Suppress("LongParameterList")
class SyncPreferenceView(
private val syncPreference: Preference,
lifecycleOwner: LifecycleOwner,
accountManager: FxaAccountManager,
private val navController: NavController
private val syncEngine: SyncEngine,
private val onSignInToSyncClicked: () -> Unit = {},
private val onSyncStatusClicked: () -> Unit = {},
private val onReconnectClicked: () -> Unit = {}
) {
init {
@ -33,9 +49,11 @@ class SyncLoginsPreferenceView(
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
MainScope().launch { updateSyncPreferenceStatus() }
}
override fun onLoggedOut() {
MainScope().launch { updateSyncPreferenceNeedsLogin() }
}
override fun onAuthenticationProblems() {
MainScope().launch { updateSyncPreferenceNeedsReauth() }
}
@ -43,6 +61,7 @@ class SyncLoginsPreferenceView(
val accountExists = accountManager.authenticatedAccount() != null
val needsReauth = accountManager.accountNeedsReauth()
when {
needsReauth -> updateSyncPreferenceNeedsReauth()
accountExists -> updateSyncPreferenceStatus()
@ -51,62 +70,50 @@ class SyncLoginsPreferenceView(
}
/**
* Show the current status of the sync preference (on/off) for the logged in user.
* Shows the current status of the sync preference ("On"/"Off") for the logged in user.
*/
private fun updateSyncPreferenceStatus() {
syncLoginsPreference.apply {
syncPreference.apply {
val syncEnginesStatus = SyncEnginesStorage(context).getStatus()
val loginsSyncStatus = syncEnginesStatus.getOrElse(SyncEngine.Passwords) { false }
val loginsSyncStatus = syncEnginesStatus.getOrElse(syncEngine) { false }
summary = context.getString(
if (loginsSyncStatus) R.string.preferences_passwords_sync_logins_on
else R.string.preferences_passwords_sync_logins_off
)
setOnPreferenceClickListener {
navigateToAccountSettingsFragment()
onSyncStatusClicked()
true
}
}
}
/**
* Indicate that the user can sign in to turn on sync.
* Display that the user can "Sign in to Sync" when the user is logged off.
*/
private fun updateSyncPreferenceNeedsLogin() {
syncLoginsPreference.apply {
syncPreference.apply {
summary = context.getString(R.string.preferences_passwords_sync_logins_sign_in)
setOnPreferenceClickListener {
navigateToTurnOnSyncFragment()
onSignInToSyncClicked()
true
}
}
}
/**
* Indicate that the user can fix their account problems to turn on sync.
* Displays that the user needs to "Reconnect" to fix their account problems with sync.
*/
private fun updateSyncPreferenceNeedsReauth() {
syncLoginsPreference.apply {
syncPreference.apply {
summary = context.getString(R.string.preferences_passwords_sync_logins_reconnect)
setOnPreferenceClickListener {
navigateToAccountProblemFragment()
onReconnectClicked()
true
}
}
}
private fun navigateToAccountSettingsFragment() {
val directions =
SavedLoginsAuthFragmentDirections.actionGlobalAccountSettingsFragment()
navController.navigate(directions)
}
private fun navigateToAccountProblemFragment() {
val directions = SavedLoginsAuthFragmentDirections.actionGlobalAccountProblemFragment()
navController.navigate(directions)
}
private fun navigateToTurnOnSyncFragment() {
val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToTurnOnSyncFragment()
navController.navigate(directions)
}
}

@ -7,6 +7,7 @@ package org.mozilla.fenix.settings.advanced
import android.app.Activity
import android.content.Context
import mozilla.components.support.locale.LocaleManager
import mozilla.components.support.locale.LocaleUseCases
import java.util.Locale
interface LocaleSettingsController {
@ -17,7 +18,8 @@ interface LocaleSettingsController {
class DefaultLocaleSettingsController(
private val activity: Activity,
private val localeSettingsStore: LocaleSettingsStore
private val localeSettingsStore: LocaleSettingsStore,
private val localeUseCase: LocaleUseCases
) : LocaleSettingsController {
override fun handleLocaleSelected(locale: Locale) {
@ -26,7 +28,7 @@ class DefaultLocaleSettingsController(
return
}
localeSettingsStore.dispatch(LocaleSettingsAction.Select(locale))
LocaleManager.setNewLocale(activity, locale.toLanguageTag())
LocaleManager.setNewLocale(activity, localeUseCase, locale)
LocaleManager.updateBaseConfiguration(activity, locale)
activity.recreate()
}
@ -36,7 +38,7 @@ class DefaultLocaleSettingsController(
return
}
localeSettingsStore.dispatch(LocaleSettingsAction.Select(localeSettingsStore.state.localeList[0]))
LocaleManager.resetToSystemDefault(activity)
LocaleManager.resetToSystemDefault(activity, localeUseCase)
LocaleManager.updateBaseConfiguration(activity, localeSettingsStore.state.localeList[0])
activity.recreate()
}

@ -17,13 +17,15 @@ import kotlinx.android.synthetic.main.fragment_locale_settings.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.locale.LocaleUseCases
import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.showToolbar
class LocaleSettingsFragment : Fragment() {
private lateinit var store: LocaleSettingsStore
private lateinit var localeSettingsStore: LocaleSettingsStore
private lateinit var interactor: LocaleSettingsInteractor
private lateinit var localeView: LocaleSettingsView
@ -39,7 +41,10 @@ class LocaleSettingsFragment : Fragment() {
): View? {
val view = inflater.inflate(R.layout.fragment_locale_settings, container, false)
store = StoreProvider.get(this) {
val browserStore = requireContext().components.core.store
val localeUseCase = LocaleUseCases(browserStore)
localeSettingsStore = StoreProvider.get(this) {
LocaleSettingsStore(
createInitialLocaleSettingsState(requireContext())
)
@ -47,7 +52,8 @@ class LocaleSettingsFragment : Fragment() {
interactor = LocaleSettingsInteractor(
controller = DefaultLocaleSettingsController(
activity = requireActivity(),
localeSettingsStore = store
localeSettingsStore = localeSettingsStore,
localeUseCase = localeUseCase
)
)
localeView = LocaleSettingsView(view.locale_container, interactor)
@ -87,7 +93,7 @@ class LocaleSettingsFragment : Fragment() {
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
consumeFrom(store) {
consumeFrom(localeSettingsStore) {
localeView.update(it)
}
}

@ -20,16 +20,231 @@ class LocaleViewHolder(
) : BaseLocaleViewHolder(view, selectedLocale) {
override fun bind(locale: Locale) {
if (locale.toString().equals("vec", ignoreCase = true)) {
locale.toString()
}
// Capitalisation is done using the rules of the appropriate locale (endonym and exonym).
locale_title_text.text = locale.getDisplayName(locale).capitalize(locale)
locale_title_text.text = getDisplayName(locale)
// Show the given locale using the device locale for the subtitle.
locale_subtitle_text.text = locale.displayName.capitalize(Locale.getDefault())
locale_subtitle_text.text = locale.getProperDisplayName()
locale_selected_icon.isVisible = isCurrentLocaleSelected(locale, isDefault = false)
itemView.setOnClickListener {
interactor.onLocaleSelected(locale)
}
}
private fun getDisplayName(locale: Locale): String {
val displayName = locale.getDisplayName(locale).capitalize(locale)
if (displayName.equals(locale.toString(), ignoreCase = true)) {
return LOCALE_TO_DISPLAY_NATIVE_NAME_MAP[locale.toString()] ?: displayName
}
return displayName
}
@SuppressWarnings("LargeClass")
companion object {
val LOCALE_TO_DISPLAY_NATIVE_NAME_MAP: Map<String, String> = mapOf(
"an" to "Aragonés",
"anp" to "अंगिका",
"ar" to "العربية",
"ast" to "Asturianu",
"ay" to "Aimara",
"az" to "Azərbaycan dili",
"be" to "беларуская мова",
"bg" to "български език",
"bn" to "বাংলা",
"br" to "Brezhoneg",
"bs" to "Bosanski jezik",
"ca" to "Català",
"cak" to "Kaqchikel",
"ceb" to "Cebuano",
"co" to "Corsu, ",
"cs" to "čeština",
"cy" to "Cymraeg",
"da" to "dansk",
"de" to "Deutsch",
"dsb" to "dolnoserbski",
"el" to "ελληνικά",
"eo" to "Esperanto",
"es" to "Español",
"et" to "Eesti",
"eu" to "Euskara",
"fa" to "فارسی",
"ff" to "Fulfulde",
"fi" to "Suomi",
"fr" to "Français",
"fy-NL" to "Frisian",
"ga-IE" to "Gaeilge",
"gd" to "Gàidhlig",
"gl" to "Galego",
"gn" to "Avañe'ẽ",
"gu-IN" to "ગુજરાતી",
"he" to "עברית",
"hi-IN" to "हिन्दी ",
"hil" to "Ilonggo",
"hr" to "hrvatski jezik",
"hsb" to "Hornjoserbsce",
"hu" to "Magyar",
"hus" to "Tének",
"hy-AM" to "հայերեն",
"ia" to "Interlingua",
"id" to "Bahasa Indonesia",
"is" to "Íslenska",
"it" to "Italiano",
"ixl" to "Ixil",
"ja" to "日本語 (にほんご)",
"jv" to "Basa Jawa",
"ka" to "ქართული",
"kab" to "Taqbaylit",
"kk" to "қазақ тілі",
"kmr" to "Kurmancî",
"kn" to "ಕನ್ನಡ",
"ko" to "한국어",
"lij" to "Ligure",
"lo" to "ພາສາລາວ",
"lt" to "lietuvių kalba",
"mix" to "Tu'un savi",
"ml" to "മലയാളം",
"mr" to "मराठी",
"ms" to "Bahasa Melayu ملايو‎",
"my" to "ဗမာစာ",
"meh" to "Tu´un savi ñuu Yasi'í Yuku Iti",
"nb-NO" to "Bokmål",
"ne-NP" to "नेपाली",
"nl" to "Nederlands",
"nn-NO" to "Nynorsk",
"nv" to "Diné bizaad",
"oc" to "Occitan",
"pa-IN" to "Panjābī",
"pl" to "Polszczyzna",
"pt" to "Português",
"pai" to "Paa ipai",
"ppl" to "Náhuat Pipil",
"quy" to "Chanka Qhichwa",
"quc" to "K'iche'",
"rm" to "Rumantsch Grischun",
"ro" to "Română",
"ru" to "русский",
"sat" to "ᱥᱟᱱᱛᱟᱲᱤ",
"sk" to "Slovak",
"sl" to "Slovenian",
"sn" to "ChiShona",
"sq" to "Shqip",
"sr" to "српски језик",
"su" to "Basa Sunda",
"sv-SE" to "Svenska",
"ta" to "தமிழ்",
"te" to "తెలుగు",
"tg" to "тоҷикӣ, toçikī, تاجیکی‎",
"th" to "ไทย",
"tl" to "Wikang Tagalog",
"tr" to "Türkçe",
"trs" to "Triqui",
"tt" to "татарча",
"tsz" to "P'urhepecha",
"uk" to "Українська",
"ur" to "اردو",
"uz" to "Oʻzbek",
"vec" to "Vèneto",
"vi" to "Tiếng Việt",
"wo" to "Wolof",
"zam" to "Ɂztè"
)
val LOCALE_TO_DISPLAY_ENGLISH_NAME_MAP: Map<String, String> = mapOf(
"an" to "Aragonese",
"ar" to "Arabic",
"ast" to "Asturianu",
"az" to "Azerbaijani",
"be" to "Belarusian",
"bg" to "Bulgarian",
"bn" to "Bengali",
"br" to "Breton",
"bs" to "Bosnian",
"ca" to "Catalan",
"cak" to "Kaqchikel",
"ceb" to "Cebuano",
"co" to "Corsican",
"cs" to "Czech",
"cy" to "Welsh",
"da" to "Danish",
"de" to "German",
"dsb" to "Sorbian, Lower",
"el" to "Greek",
"eo" to "Esperanto",
"es" to "Spanish",
"et" to "Estonian",
"eu" to "Basque",
"fa" to "Persian",
"ff" to "Fulah",
"fi" to "Finnish",
"fr" to "French",
"fy-NL" to "Frisian",
"ga-IE" to "Irish",
"gd" to "Gaelic",
"gl" to "Galician",
"gn" to "Guarani",
"gu-IN" to "Gujarati",
"he" to "Hebrew",
"hi-IN" to "Hindi",
"hil" to "Hiligaynon",
"hr" to "Croatian",
"hsb" to "Sorbian, Upper",
"hu" to "Hungarian",
"hy-AM" to "Armenian",
"id" to "Indonesian",
"is" to "Icelandic",
"it" to "Italian",
"ja" to "Japanese",
"ka" to "Georgian",
"kab" to "Kabyle ",
"kk" to "Kazakh",
"kmr" to "Kurmanji Kurdish",
"kn" to "Kannada",
"ko" to "Korean",
"lij" to "Ligurian",
"lo" to "Lao",
"lt" to "Lithuanian",
"mix" to "Mixtepec Mixtec",
"ml" to "Malayalam",
"mr" to "Marathi",
"ms" to "Malay",
"my" to "Burmese",
"nb-NO" to "Norwegian Bokmål",
"ne-NP" to "Nepali ",
"nl" to "Dutch, Flemish",
"nn-NO" to "Norwegian Nynorsk",
"nv" to "Navajo, Navaho",
"oc" to "Occitan",
"pa-IN" to "Punjabi",
"pl" to "Polish",
"pt-BR" to "",
"pt-PT" to "",
"rm" to "Romansh",
"ro" to "Română",
"ru" to "Russian",
"sat" to "Santali",
"sk" to "Slovak",
"sl" to "Slovenian",
"sq" to "Albanian",
"sr" to "Serbian",
"su" to "Sundanese",
"sv-SE" to "Swedish",
"ta" to "Tamil",
"te" to "Telugu",
"tg" to "Tajik",
"th" to "Thai",
"tl" to "Tagalog",
"tr" to "Turkish",
"trs" to "Triqui",
"uk" to "Ukrainian",
"ur" to "Urdu",
"uz" to "Uzbek",
"vec" to "Venitian",
"vi" to "Vietnamese"
)
}
}
class SystemLocaleViewHolder(
@ -72,3 +287,14 @@ abstract class BaseLocaleViewHolder(
private fun String.capitalize(locale: Locale): String {
return substring(0, 1).toUpperCase(locale) + substring(1)
}
/**
* Returns the locale in the selected language, with fallback to English name
*/
private fun Locale.getProperDisplayName(): String {
val displayName = this.displayName.capitalize(Locale.getDefault())
if (displayName.equals(this.toString(), ignoreCase = true)) {
return LocaleViewHolder.LOCALE_TO_DISPLAY_ENGLISH_NAME_MAP[this.toString()] ?: displayName
}
return displayName
}

@ -2,7 +2,7 @@
* 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.settings.logins.biometric
package org.mozilla.fenix.settings.biometric
import android.content.Context
import android.os.Build.VERSION.SDK_INT
@ -16,8 +16,8 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.settings.logins.biometric.ext.isEnrolled
import org.mozilla.fenix.settings.logins.biometric.ext.isHardwareAvailable
import org.mozilla.fenix.settings.biometric.ext.isEnrolled
import org.mozilla.fenix.settings.biometric.ext.isHardwareAvailable
/**
* A [LifecycleAwareFeature] for the Android Biometric API to prompt for user authentication.

@ -2,7 +2,7 @@
* 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.settings.logins.biometric.ext
package org.mozilla.fenix.settings.biometric.ext
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK

@ -22,18 +22,19 @@ import androidx.preference.SwitchPreference
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import mozilla.components.service.fxa.SyncEngine
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.secure
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.settings.SharedPreferenceUpdater
import org.mozilla.fenix.settings.logins.biometric.BiometricPromptFeature
import org.mozilla.fenix.settings.logins.SyncLoginsPreferenceView
import org.mozilla.fenix.settings.SyncPreferenceView
import org.mozilla.fenix.settings.biometric.BiometricPromptFeature
import org.mozilla.fenix.settings.requirePreference
@Suppress("TooManyFunctions")
@ -121,11 +122,26 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() {
true
}
SyncLoginsPreferenceView(
requirePreference(R.string.pref_key_password_sync_logins),
SyncPreferenceView(
syncPreference = requirePreference(R.string.pref_key_password_sync_logins),
lifecycleOwner = viewLifecycleOwner,
accountManager = requireComponents.backgroundServices.accountManager,
navController = findNavController()
syncEngine = SyncEngine.Passwords,
onSignInToSyncClicked = {
val directions =
SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToTurnOnSyncFragment()
findNavController().navigate(directions)
},
onSyncStatusClicked = {
val directions =
SavedLoginsAuthFragmentDirections.actionGlobalAccountSettingsFragment()
findNavController().navigate(directions)
},
onReconnectClicked = {
val directions =
SavedLoginsAuthFragmentDirections.actionGlobalAccountProblemFragment()
findNavController().navigate(directions)
}
)
togglePrefsEnabledWhileAuthenticating(true)

@ -64,11 +64,6 @@ class SavedLoginsFragment : Fragment() {
initToolbar()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -164,6 +159,7 @@ class SavedLoginsFragment : Fragment() {
) = (activity as HomeActivity).openToBrowserAndLoad(searchTermOrURL, newTab, from)
private fun initToolbar() {
setHasOptionsMenu(true)
showToolbar(getString(R.string.preferences_passwords_saved_logins))
(activity as HomeActivity).getSupportActionBarAndInflateIfNecessary()
.setDisplayShowTitleEnabled(false)

@ -22,6 +22,7 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.icons.IconRequest
import mozilla.components.browser.state.search.SearchEngine
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
@ -118,7 +119,8 @@ class EditCustomSearchEngineFragment : Fragment(R.layout.fragment_add_search_eng
SearchStringValidator.Result.Success -> {
val update = searchEngine.copy(
name = name,
resultUrls = listOf(searchString.toSearchUrl())
resultUrls = listOf(searchString.toSearchUrl()),
icon = requireComponents.core.icons.loadIcon(IconRequest(searchString)).await().bitmap
)
requireComponents.useCases.searchUseCases.addSearchEngine(update)

@ -0,0 +1,96 @@
/* 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.tabstray
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.navigation.fragment.findNavController
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayout
import kotlinx.android.synthetic.main.component_tabstray2.*
import kotlinx.android.synthetic.main.component_tabstray2.view.*
import org.mozilla.fenix.R
class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
lateinit var behavior: BottomSheetBehavior<ConstraintLayout>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, R.style.TabTrayDialogStyle)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val containerView = inflater.inflate(R.layout.fragment_tab_tray_dialog, container, false)
val view: View = LayoutInflater.from(containerView.context)
.inflate(R.layout.component_tabstray2, containerView as ViewGroup, true)
behavior = BottomSheetBehavior.from(view.tab_wrapper)
return containerView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupPager(view.context, this)
}
override fun setCurrentTrayPosition(position: Int) {
tabsTray.currentItem = position
}
override fun navigateToBrowser() {
dismissAllowingStateLoss()
val navController = findNavController()
if (navController.currentDestination?.id == R.id.browserFragment) {
return
}
if (!navController.popBackStack(R.id.browserFragment, false)) {
navController.navigate(R.id.browserFragment)
}
}
override fun tabRemoved(sessionId: String) {
// TODO re-implement these methods
// showUndoSnackbarForTab(sessionId)
// removeIfNotLastTab(sessionId)
}
private fun setupPager(context: Context, interactor: TabsTrayInteractor) {
tabsTray.apply {
adapter = TrayPagerAdapter(context, interactor)
isUserInputEnabled = false
}
tab_layout.addOnTabSelectedListener(TabLayoutObserver(interactor))
}
}
/**
* An observer for the [TabLayout] used for the Tabs Tray.
*/
internal class TabLayoutObserver(
private val interactor: TabsTrayInteractor
) : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
interactor.setCurrentTrayPosition(tab.position)
}
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
override fun onTabReselected(tab: TabLayout.Tab) = Unit
}

@ -0,0 +1,22 @@
/* 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.tabstray
interface TabsTrayInteractor {
/**
* Set the current tray item to the clamped [position].
*/
fun setCurrentTrayPosition(position: Int)
/**
* Dismisses the tabs tray and navigates to the browser.
*/
fun navigateToBrowser()
/**
* Invoked when a tab is removed from the tabs tray with the given [sessionId].
*/
fun tabRemoved(sessionId: String)
}

@ -0,0 +1,13 @@
/* 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.tabstray
import android.view.View
import android.view.ViewGroup
/**
* A [View] or [ViewGroup] that can be add in the Tabs Tray.
*/
interface TrayItem

@ -0,0 +1,59 @@
/* 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.tabstray
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.tabstray.BrowserTabViewHolder.Companion.LAYOUT_ID_NORMAL_TAB
import org.mozilla.fenix.tabstray.BrowserTabViewHolder.Companion.LAYOUT_ID_PRIVATE_TAB
import org.mozilla.fenix.tabtray.FenixTabsAdapter
class TrayPagerAdapter(
context: Context,
val interactor: TabsTrayInteractor
) : RecyclerView.Adapter<TrayViewHolder>() {
private val normalAdapter by lazy { FenixTabsAdapter(context) }
private val privateAdapter by lazy { FenixTabsAdapter(context) }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrayViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
LAYOUT_ID_NORMAL_TAB -> BrowserTabViewHolder(itemView, interactor)
LAYOUT_ID_PRIVATE_TAB -> BrowserTabViewHolder(itemView, interactor)
else -> throw IllegalStateException("Unknown viewType.")
}
}
override fun onBindViewHolder(viewHolder: TrayViewHolder, position: Int) {
val adapter = when (position) {
POSITION_NORMAL_TABS -> normalAdapter
POSITION_PRIVATE_TABS -> privateAdapter
else -> throw IllegalStateException("View type does not exist.")
}
viewHolder.bind(adapter)
}
override fun getItemViewType(position: Int): Int {
return when (position) {
POSITION_NORMAL_TABS -> LAYOUT_ID_NORMAL_TAB
POSITION_PRIVATE_TABS -> LAYOUT_ID_PRIVATE_TAB
else -> throw IllegalStateException("Unknown position.")
}
}
override fun getItemCount(): Int = TRAY_TABS_COUNT
companion object {
const val TRAY_TABS_COUNT = 2
const val POSITION_NORMAL_TABS = 0
const val POSITION_PRIVATE_TABS = 1
}
}

@ -0,0 +1,41 @@
/* 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.tabstray
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
import org.mozilla.fenix.R
import org.mozilla.fenix.tabstray.browser.BaseBrowserTrayList
sealed class TrayViewHolder constructor(
override val containerView: View
) : RecyclerView.ViewHolder(containerView), LayoutContainer {
abstract fun bind(adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>)
}
class BrowserTabViewHolder(
containerView: View,
interactor: TabsTrayInteractor
) : TrayViewHolder(containerView) {
private val trayList: BaseBrowserTrayList = itemView.findViewById(R.id.tray_list_item)
init {
trayList.interactor = interactor
}
override fun bind(adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>) {
trayList.layoutManager = LinearLayoutManager(itemView.context)
trayList.adapter = adapter
}
companion object {
const val LAYOUT_ID_NORMAL_TAB = R.layout.normal_browser_tray_list
const val LAYOUT_ID_PRIVATE_TAB = R.layout.private_browser_tray_list
}
}

@ -0,0 +1,95 @@
/* 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.tabstray.browser
import android.content.Context
import android.util.AttributeSet
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.tabstray.TabsAdapter
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.tabs.tabstray.TabsFeature
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.tabstray.TabsTrayInteractor
import org.mozilla.fenix.tabstray.TrayItem
import org.mozilla.fenix.tabstray.ext.filterFromConfig
import org.mozilla.fenix.utils.view.LifecycleViewProvider
abstract class BaseBrowserTrayList @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr), TrayItem {
enum class BrowserTabType { NORMAL, PRIVATE }
data class Configuration(val browserTabType: BrowserTabType)
abstract val configuration: Configuration
var interactor: TabsTrayInteractor? = null
private val lifecycleProvider = LifecycleViewProvider(this)
private val selectTabUseCase = SelectTabUseCaseWrapper(
context.components.analytics.metrics,
context.components.useCases.tabsUseCases.selectTab
) {
interactor?.navigateToBrowser()
}
private val removeTabUseCase = RemoveTabUseCaseWrapper(
context.components.analytics.metrics
) { sessionId ->
interactor?.tabRemoved(sessionId)
}
private val tabsFeature by lazy {
ViewBoundFeatureWrapper(
feature = TabsFeature(
adapter as TabsAdapter,
context.components.core.store,
selectTabUseCase,
removeTabUseCase,
{ it.filterFromConfig(configuration) },
{ }
),
owner = lifecycleProvider,
view = this
)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
// This is weird, but I don't have a better solution right now: We need to keep a
// lazy reference to the feature/adapter so that we do not re-create
// it every time it's attached. This reference is our way to init.
tabsFeature
}
}
internal class SelectTabUseCaseWrapper(
private val metrics: MetricController,
private val selectTab: TabsUseCases.SelectTabUseCase,
private val onSelect: (String) -> Unit
) : TabsUseCases.SelectTabUseCase {
override fun invoke(tabId: String) {
metrics.track(Event.OpenedExistingTab)
selectTab(tabId)
onSelect(tabId)
}
}
internal class RemoveTabUseCaseWrapper(
private val metrics: MetricController,
private val onRemove: (String) -> Unit
) : TabsUseCases.RemoveTabUseCase {
override fun invoke(sessionId: String) {
metrics.track(Event.ClosedExistingTab)
onRemove(sessionId)
}
}

@ -0,0 +1,19 @@
/* 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.tabstray.browser
import android.content.Context
import android.util.AttributeSet
/**
* A browser tabs list that displays normal tabs.
*/
class NormalBrowserTrayList @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseBrowserTrayList(context, attrs, defStyleAttr) {
override val configuration: Configuration = Configuration(BrowserTabType.NORMAL)
}

@ -0,0 +1,19 @@
/* 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.tabstray.browser
import android.content.Context
import android.util.AttributeSet
/**
* A browser tabs list that displays private tabs.
*/
class PrivateBrowserTrayList @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseBrowserTrayList(context, attrs, defStyleAttr) {
override val configuration: Configuration = Configuration(BrowserTabType.PRIVATE)
}

@ -0,0 +1,15 @@
/* 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.tabstray.ext
import mozilla.components.browser.state.state.TabSessionState
import org.mozilla.fenix.tabstray.browser.BaseBrowserTrayList.BrowserTabType.PRIVATE
import org.mozilla.fenix.tabstray.browser.BaseBrowserTrayList.Configuration
fun TabSessionState.filterFromConfig(configuration: Configuration): Boolean {
val isPrivate = configuration.browserTabType == PRIVATE
return content.private == isPrivate
}

@ -15,6 +15,7 @@ import kotlinx.android.synthetic.main.tab_tray_item.view.*
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.browser.tabstray.TabsAdapter
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
import mozilla.components.concept.base.images.ImageLoader
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.Tabs
@ -27,7 +28,7 @@ import org.mozilla.fenix.ext.updateAccessibilityCollectionItemInfo
class FenixTabsAdapter(
private val context: Context,
imageLoader: ImageLoader
imageLoader: ImageLoader = ThumbnailLoader(context.components.core.thumbnailStorage)
) : TabsAdapter(
viewHolderProvider = { parentView ->
TabTrayViewHolder(

@ -23,7 +23,7 @@ import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.ViewHolder
*/
class SaveToCollectionsButtonAdapter(
private val interactor: TabTrayInteractor,
private val isPrivate: Boolean = false
private val isPrivate: () -> Boolean = { false }
) : ListAdapter<Item, ViewHolder>(DiffCallback) {
init {
@ -66,7 +66,7 @@ class SaveToCollectionsButtonAdapter(
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.itemView.isVisible = !isPrivate &&
holder.itemView.isVisible = !isPrivate() &&
interactor.onModeRequested() is TabTrayDialogFragmentState.Mode.Normal
}

@ -92,7 +92,8 @@ class TabTrayView(
private var multiselectMenu: BrowserMenu? = null
private var tabsTouchHelper: TabsTouchHelper
private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate)
private val collectionsButtonAdapter =
SaveToCollectionsButtonAdapter(interactor) { isPrivateModeSelected }
private var hasLoaded = false
@ -700,7 +701,6 @@ class TabTrayView(
fun scrollToSelectedBrowserTab(selectedTabId: String? = null) {
view.tabsTray.apply {
val recyclerViewIndex = getSelectedBrowserTabViewIndex(selectedTabId)
layoutManager?.scrollToPosition(recyclerViewIndex)
smoothScrollBy(
0,

@ -0,0 +1,32 @@
/* 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.utils
import android.content.Context
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
/**
* A GridLayoutManager that can be used to override methods in Android implementation
* to improve ayy1 or fix a11y issues.
*/
class AccessibilityGridLayoutManager(
context: Context,
spanCount: Int
) : GridLayoutManager(
context,
spanCount
) {
override fun getColumnCountForAccessibility(
recycler: RecyclerView.Recycler,
state: RecyclerView.State
): Int {
return if (itemCount < spanCount) {
itemCount
} else {
super.getColumnCountForAccessibility(recycler, state)
}
}
}

@ -26,10 +26,12 @@ import mozilla.components.support.ktx.android.content.longPreference
import mozilla.components.support.ktx.android.content.stringPreference
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.metrics.MozillaProductDetector
import org.mozilla.fenix.components.settings.counterPreference
import org.mozilla.fenix.components.settings.featureFlagPreference
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
@ -319,6 +321,12 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = false
)
var tabsTrayRewrite by featureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_new_tabs_tray),
default = false,
featureFlag = FeatureFlags.tabsTrayRewrite
)
fun getTabTimeout(): Long = when {
closeTabsAfterOneDay -> ONE_DAY_MS
closeTabsAfterOneWeek -> ONE_WEEK_MS
@ -949,6 +957,38 @@ class Settings(private val appContext: Context) : PreferencesHolder {
0
)
/**
* Storing number of installed add-ons for telemetry purposes
*/
var installedAddonsCount by intPreference(
appContext.getPreferenceKey(R.string.pref_key_installed_addons_count),
0
)
/**
* Storing the list of installed add-ons for telemetry purposes
*/
var installedAddonsList by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_installed_addons_list),
default = ""
)
/**
* Storing number of enabled add-ons for telemetry purposes
*/
var enabledAddonsCount by intPreference(
appContext.getPreferenceKey(R.string.pref_key_enabled_addons_count),
0
)
/**
* Storing the list of enabled add-ons for telemetry purposes
*/
var enabledAddonsList by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_enabled_addons_list),
default = ""
)
private var savedLoginsSortingStrategyString by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_saved_logins_sorting_strategy),
default = SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort.strategyString
@ -988,4 +1028,16 @@ class Settings(private val appContext: Context) : PreferencesHolder {
appContext.getPreferenceKey(R.string.pref_key_swipe_toolbar_switch_tabs),
default = true
)
var creditCardsFeature by featureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_show_credit_cards_feature),
default = false,
featureFlag = FeatureFlags.creditCardsFeature
)
var addressFeature by featureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_show_address_feature),
default = false,
featureFlag = FeatureFlags.addressesFeature
)
}

@ -16,18 +16,18 @@ import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.browser_toolbar_popup_window.view.*
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.store.BrowserStore
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import java.lang.ref.WeakReference
import mozilla.components.browser.state.selector.findCustomTab
object ToolbarPopupWindow {
fun show(
view: WeakReference<View>,
customTabSession: CustomTabSessionState? = null,
customTabId: String? = null,
handlePasteAndGo: (String) -> Unit,
handlePaste: (String) -> Unit,
copyVisible: Boolean = true
@ -36,7 +36,7 @@ object ToolbarPopupWindow {
val clipboard = context.components.clipboardHandler
if (!copyVisible && clipboard.text.isNullOrEmpty()) return
val isCustomTabSession = customTabSession != null
val isCustomTabSession = customTabId != null
val customView = LayoutInflater.from(context)
.inflate(R.layout.browser_toolbar_popup_window, null)
@ -63,7 +63,7 @@ object ToolbarPopupWindow {
popupWindow.dismiss()
clipboard.text = getUrlForClipboard(
it.context.components.core.store,
customTabSession
customTabId
)
view.get()?.let {
@ -101,10 +101,11 @@ object ToolbarPopupWindow {
@VisibleForTesting
internal fun getUrlForClipboard(
store: BrowserStore,
customTabSession: CustomTabSessionState? = null
customTabId: String? = null
): String? {
return if (customTabSession != null) {
customTabSession.content.url
return if (customTabId != null) {
val customTab = store.state.findCustomTab(customTabId)
customTab?.content?.url
} else {
val selectedTab = store.state.selectedTab
selectedTab?.readerState?.activeUrl ?: selectedTab?.content?.url

@ -0,0 +1,45 @@
/* 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.utils.view
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Lifecycle.State
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
/**
* Provides a [LifecycleOwner] on a given [View] for features that function on lifecycle events.
*
* When the [View] is attached to the window, observers will receive the [Lifecycle.Event.ON_START] event.
* When the [View] is detached to the window, observers will receive the [Lifecycle.Event.ON_STOP] event.
*
* @param view The [View] that will be observed.
*/
class LifecycleViewProvider(view: View) : LifecycleOwner {
private val registry = LifecycleRegistry(this)
init {
registry.currentState = State.INITIALIZED
view.addOnAttachStateChangeListener(ViewBinding(registry))
}
override fun getLifecycle(): Lifecycle = registry
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal class ViewBinding(
private val registry: LifecycleRegistry
) : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
registry.currentState = State.STARTED
}
override fun onViewDetachedFromWindow(v: View?) {
registry.currentState = State.DESTROYED
}
}

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#08a4ff"
android:pathData="M 2 0 L 22 0 C 22.53 0 23.039 0.211 23.414 0.586 C 23.789 0.961 24 1.47 24 2 L 24 22 C 24 22.53 23.789 23.039 23.414 23.414 C 23.039 23.789 22.53 24 22 24 L 2 24 C 1.47 24 0.961 23.789 0.586 23.414 C 0.211 23.039 0 22.53 0 22 L 0 2 C 0 1.47 0.211 0.961 0.586 0.586 C 0.961 0.211 1.47 0 2 0"
android:strokeWidth="1" />
<path
android:name="path_1"
android:fillColor="#ffffff"
android:pathData="M 19.707 18.293 L 14.885 13.471 C 16.7 10.921 16.257 7.406 13.868 5.385 C 11.478 3.364 7.938 3.512 5.725 5.725 C 3.512 7.938 3.364 11.478 5.385 13.868 C 7.406 16.257 10.921 16.7 13.471 14.885 L 18.293 19.707 C 18.685 20.086 19.309 20.081 19.695 19.695 C 20.081 19.309 20.086 18.685 19.707 18.293 Z M 10 14 C 7.791 14 6 12.209 6 10 C 6 7.791 7.791 6 10 6 C 12.209 6 14 7.791 14 10 C 14 11.061 13.579 12.078 12.828 12.828 C 12.078 13.579 11.061 14 10 14 Z"
android:strokeWidth="1" />
</vector>

@ -0,0 +1,174 @@
<?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/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tab_wrapper"
style="@style/BottomSheetModal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:backgroundTint="@color/foundation_normal_theme"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
tools:ignore="MozMultipleConstraintLayouts">
<View
android:id="@+id/handle"
android:layout_width="0dp"
android:layout_height="@dimen/bottom_sheet_handle_height"
android:layout_marginTop="@dimen/bottom_sheet_handle_top_margin"
android:background="@color/secondary_text_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.1" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/infoBanner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/foundation_normal_theme"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/topBar" />
<TextView
android:id="@+id/tab_tray_empty_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center_horizontal"
android:paddingTop="80dp"
android:text="@string/no_open_tabs_description"
android:textColor="?secondaryText"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/infoBanner" />
<View
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="@color/foundation_normal_theme"
android:importantForAccessibility="no"
app:layout_constraintTop_toBottomOf="@+id/handle" />
<ImageButton
android:id="@+id/exit_multi_select"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_close_multiselect_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/multiselect_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/multiselect_title"
app:srcCompat="@drawable/ic_close"
app:tint="@color/contrast_text_normal_theme" />
<TextView
android:id="@+id/multiselect_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:focusableInTouchMode="true"
android:textColor="@color/contrast_text_normal_theme"
android:textSize="20sp"
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintBottom_toBottomOf="@id/topBar"
app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/exit_multi_select"
app:layout_constraintTop_toTopOf="@id/topBar"
tools:text="3 selected" />
<include layout="@layout/tabstray_multiselect_items" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="0dp"
android:layout_height="80dp"
android:background="@color/foundation_normal_theme"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/handle"
app:layout_constraintWidth_percent="0.5"
app:tabGravity="fill"
app:tabIconTint="@color/tab_icon"
app:tabIndicatorColor="@color/accent_normal_theme"
app:tabMaxWidth="0dp"
app:tabRippleColor="@android:color/transparent">
<com.google.android.material.tabs.TabItem
android:id="@+id/default_tab_item"
android:layout_width="0dp"
android:layout_height="match_parent"
android:contentDescription="@string/tab_header_label"
android:layout="@layout/tabs_tray_tab_counter"
app:tabIconTint="@color/tab_icon" />
<com.google.android.material.tabs.TabItem
android:id="@+id/private_tab_item"
android:layout_width="0dp"
android:layout_height="match_parent"
android:contentDescription="@string/tabs_header_private_tabs_title"
android:icon="@drawable/ic_private_browsing" />
</com.google.android.material.tabs.TabLayout>
<ImageButton
android:id="@+id/tab_tray_new_tab"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/add_tab"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:srcCompat="@drawable/ic_new"
app:tint="@color/primary_text_normal_theme" />
<ImageButton
android:id="@+id/tab_tray_overflow"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/open_tabs_menu"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:srcCompat="@drawable/ic_menu"
app:tint="@color/tab_tray_heading_icon_menu_normal_theme" />
<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@color/tab_tray_item_divider_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/infoBanner" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/tabsTray"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingBottom="140dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider" />
</androidx.constraintlayout.widget.ConstraintLayout>

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

Loading…
Cancel
Save