Merge remote-tracking branch 'upstream/master' into fork

pull/35/head
Adam Novak 4 years ago
commit e13b236588

18
.github/CODEOWNERS vendored

@ -23,3 +23,21 @@
/automation/ @mozilla-mobile/releng @mozilla-mobile/fenix /automation/ @mozilla-mobile/releng @mozilla-mobile/fenix
/taskcluster/ /@mozilla-mobile/releng @mozilla-mobile/fenix /taskcluster/ /@mozilla-mobile/releng @mozilla-mobile/fenix
/.github/ @mozilla-mobile/releng @mozilla-mobile/fenix /.github/ @mozilla-mobile/releng @mozilla-mobile/fenix
# --- PERFORMANCE START --- #
# The performance team would like to monitor some files to understand
# when performance-impacting changes occur. Our intent is not to block
# these changes (for now) but to be aware of them. Please let us know
# if the CODEOWNERS system makes this impractical. We're available at
# #perf-android-frontend on Matrix.
/app/src/*/java/org/mozilla/fenix/perf/** @mozilla-mobile/Performance
*.pro @mozilla-mobile/Performance
*proguard* @mozilla-mobile/Performance
# Possible startup regressions
*Application.kt @mozilla-mobile/Performance
# We want to be aware of new features behind flags as well as features
# about to be enabled.
FeatureFlags.kt @mozilla-mobile/Performance
# --- PERFORMANCE END --- #

@ -6,11 +6,8 @@
- [ ] **Screenshots**: This PR includes screenshots or GIFs of the changes made or an explanation of why it does not - [ ] **Screenshots**: This PR includes screenshots or GIFs of the changes made or an explanation of why it does not
- [ ] **Accessibility**: The code in this PR follows [accessibility best practices](https://github.com/mozilla-mobile/shared-docs/blob/master/android/accessibility_guide.md) or does not include any user facing features. In addition, it includes a screenshot of a successful [accessibility scan](https://play.google.com/store/apps/details?id=com.google.android.apps.accessibility.auditor&hl=en_US) to ensure no new defects are added to the product. - [ ] **Accessibility**: The code in this PR follows [accessibility best practices](https://github.com/mozilla-mobile/shared-docs/blob/master/android/accessibility_guide.md) or does not include any user facing features. In addition, it includes a screenshot of a successful [accessibility scan](https://play.google.com/store/apps/details?id=com.google.android.apps.accessibility.auditor&hl=en_US) to ensure no new defects are added to the product.
### After merge
- [ ] **Milestone**: Make sure issues finished by this pull request are added to the [milestone](https://github.com/mozilla-mobile/fenix/milestones) of the version currently in development.
### To download an APK when reviewing a PR: ### To download an APK when reviewing a PR:
1. click on Show All Checks, 1. click on Show All Checks,
2. click Details next to "Taskcluster (pull_request)" after it appears and then finishes with a green checkmark, 2. click Details next to "Taskcluster (pull_request)" after it appears and then finishes with a green checkmark,
3. click on the "Fenix - assemble" task, then click "Run Artifacts". 3. click on the "Fenix - assemble" task, then click "Run Artifacts".
4. the APK links should be on the left side of the screen, named for each CPU architecture 4. the APK links should be on the left side of the screen, named for each CPU architecture

@ -105,21 +105,23 @@ you want these variants to be:
#### Performance Build Variants #### Performance Build Variants
For accurate performance measurements, read this section! For accurate performance measurements, read this section!
If you want to analyze performance during **local development** (note: there is a non-trivial performance impact - see caveats): To analyze performance during **local development** build a production variant locally (this could either be the Nightly, beta or release). Otherwise, you could also grab a pre-existing APK if you don't need to test some local changes. Then, use the Firefox profiler to profile what you need!
- Recommendation: use a debuggable variant (see "local.properties helpers" below) with local Leanplum, Adjust, & Sentry API tokens: contact the front-end perf group for access to them
- Rationale: There are numerous performance-impacting differences between debug and release variants so we need a release variant. To profile, we also need debuggable, which is disabled by default for release variants. If API tokens are not provided, the SDKs may change their behavior in non-trivial ways.
- Caveats:
- debuggable has a non-trivial & variable impact on performance but is needed to take profiles.
- Random experiment opt-in & feature flags may impact performance (see [perf-frontend-issues#45](https://github.com/mozilla-mobile/perf-frontend-issues/issues/45) for mitigation).
- This is slower to build than debug builds because it does additional tasks (e.g. minification) similar to other release builds
If you want to run **performance tests/benchmarks** in automation or locally: For more information on how to use the profiler or how to use the build, refer to this [how to measure performance with the build](https://wiki.mozilla.org/Performance/How_to_get_started_on_Fenix)
- Recommendation: production builds. If debuggable is required, use recommendation above but note the caveat above. If your needs are not met, please contact the front-end perf group to identify a new solution.
- Rationale: like the rationale above, we need release variants so the choice is based on the debuggable flag.
For additional context on these recommendations, see [the perf build variant analysis](https://docs.google.com/document/d/1aW-m0HYncTDDiRz_2x6EjcYkjBpL9SHhhYix13Vil30/edit#). If you want to run **performance tests/benchmarks** in automation or locally use a production build since it is much closer in behavior compared to what users see in the wild.
Before you can install any release variants, **you will need to sign them:** see [Automatically signing release builds](#automatically-sign-release-builds) for details. Before you can install any release builds, **You will need to sign production build variants:** see [Automatically signing release builds](#automatically-sign-release-builds) for details.
##### Known disabled-by-default features
Some features are disabled by default when Fenix is built locally. This can be problematic at times for checking performance since you might want to know how your code behaves with those features.
The known features that are disabled by default are:
- Sentry
- Leanplum
- Adjust
- Mozilla Location Services (also known as MLS)
- Firebase Push Services
- Telemetry (only disabled by default in debug builds)
## Pre-push hooks ## Pre-push hooks
To reduce review turn-around time, we'd like all pushes to run tests locally. We'd To reduce review turn-around time, we'd like all pushes to run tests locally. We'd

@ -468,13 +468,10 @@ dependencies {
implementation Deps.androidx_lifecycle_viewmodel implementation Deps.androidx_lifecycle_viewmodel
implementation Deps.androidx_core implementation Deps.androidx_core
implementation Deps.androidx_core_ktx implementation Deps.androidx_core_ktx
implementation Deps.androidx_dynamic_animation
implementation Deps.androidx_transition implementation Deps.androidx_transition
implementation Deps.androidx_work_ktx implementation Deps.androidx_work_ktx
implementation Deps.google_material implementation Deps.google_material
implementation Deps.google_flexbox
implementation Deps.lottie implementation Deps.lottie
implementation Deps.adjust implementation Deps.adjust
@ -482,6 +479,9 @@ dependencies {
implementation Deps.google_ads_id // Required for the Google Advertising ID implementation Deps.google_ads_id // Required for the Google Advertising ID
implementation Deps.google_play_store // Required for in-app reviews
implementation Deps.google_play_core_ktx // Required for in-app reviews
androidTestImplementation Deps.uiautomator androidTestImplementation Deps.uiautomator
// Removed pending AndroidX fixes // Removed pending AndroidX fixes
androidTestImplementation "tools.fastlane:screengrab:2.0.0" androidTestImplementation "tools.fastlane:screengrab:2.0.0"

@ -7,6 +7,8 @@
<!-- The Sentry SDK is compiled against parts of the Java SDK that are not available in the Android SDK. <!-- The Sentry SDK is compiled against parts of the Java SDK that are not available in the Android SDK.
Let's just ignore issues in the Sentry code since that is a third-party dependency anyways. --> Let's just ignore issues in the Sentry code since that is a third-party dependency anyways. -->
<ignore path="**/sentry*.jar" /> <ignore path="**/sentry*.jar" />
<!-- Temporary until https://github.com/Kotlin/kotlinx.coroutines/issues/2004 is resolved. -->
<ignore path="**/kotlinx-coroutines-core-*.jar"/>
</issue> </issue>
<!-- Lints that don't apply to our translation process --> <!-- Lints that don't apply to our translation process -->
<issue id="MissingTranslation" severity="ignore" /> <issue id="MissingTranslation" severity="ignore" />

File diff suppressed because it is too large Load Diff

@ -5,7 +5,9 @@
package org.mozilla.fenix.helpers package org.mozilla.fenix.helpers
import androidx.test.espresso.intent.rule.IntentsTestRule import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiDevice
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
/** /**
@ -16,7 +18,12 @@ import org.mozilla.fenix.HomeActivity
*/ */
class HomeActivityTestRule(initialTouchMode: Boolean = false, launchActivity: Boolean = true) : class HomeActivityTestRule(initialTouchMode: Boolean = false, launchActivity: Boolean = true) :
ActivityTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity) ActivityTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity) {
override fun beforeActivityLaunched() {
super.beforeActivityLaunched()
setLongTapTimeout()
}
}
/** /**
* A [org.junit.Rule] to handle shared test set up for tests on [HomeActivity]. This adds * A [org.junit.Rule] to handle shared test set up for tests on [HomeActivity]. This adds
@ -26,5 +33,19 @@ class HomeActivityTestRule(initialTouchMode: Boolean = false, launchActivity: Bo
* @param launchActivity See [IntentsTestRule] * @param launchActivity See [IntentsTestRule]
*/ */
class HomeActivityIntentTestRule(initialTouchMode: Boolean = false, launchActivity: Boolean = true) : class HomeActivityIntentTestRule(
IntentsTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity) initialTouchMode: Boolean = false,
launchActivity: Boolean = true
) :
IntentsTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity) {
override fun beforeActivityLaunched() {
super.beforeActivityLaunched()
setLongTapTimeout()
}
}
// changing the device preference for Touch and Hold delay, to avoid long-clicks instead of a single-click
fun setLongTapTimeout() {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.executeShellCommand("settings put secure long_press_timeout 3000")
}

@ -3,13 +3,13 @@ package org.mozilla.fenix.helpers
import androidx.test.espresso.IdlingResource import androidx.test.espresso.IdlingResource
import androidx.test.espresso.IdlingResource.ResourceCallback import androidx.test.espresso.IdlingResource.ResourceCallback
class RecyclerViewIdlingResource constructor(private val recycler: androidx.recyclerview.widget.RecyclerView) : class RecyclerViewIdlingResource constructor(private val recycler: androidx.recyclerview.widget.RecyclerView, val minItemCount: Int = 0) :
IdlingResource { IdlingResource {
private var callback: ResourceCallback? = null private var callback: ResourceCallback? = null
override fun isIdleNow(): Boolean { override fun isIdleNow(): Boolean {
if (recycler.adapter != null && recycler.adapter!!.itemCount > 0) { if (recycler.adapter != null && recycler.adapter!!.itemCount > minItemCount) {
if (callback != null) { if (callback != null) {
callback!!.onTransitionToIdle() callback!!.onTransitionToIdle()
} }

@ -113,7 +113,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
verifyBookmarkedURL(defaultWebPage.url.toString()) verifyBookmarkedURL(defaultWebPage.url.toString())
@ -121,21 +121,19 @@ class BookmarksTest {
} }
} }
@Ignore("Intermittent failures: https://github.com/mozilla-mobile/fenix/issues/10911")
@Test @Test
fun createBookmarkFolderTest() { fun createBookmarkFolderTest() {
homeScreen { homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
clickAddFolderButton()
verifyKeyboardVisible()
addNewFolderName(bookmarksFolderName)
saveNewFolder()
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
clickAddFolderButton()
verifyKeyboardVisible()
addNewFolderName(bookmarksFolderName)
saveNewFolder()
verifyFolderTitle(bookmarksFolderName) verifyFolderTitle(bookmarksFolderName)
verifyKeyboardHidden() verifyKeyboardHidden()
} }
@ -163,7 +161,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
@ -193,7 +191,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
}.clickCopy { }.clickCopy {
@ -210,7 +208,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
}.clickShare { }.clickShare {
@ -230,7 +228,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
}.clickOpenInNewTab { }.clickOpenInNewTab {
@ -249,7 +247,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
}.clickOpenInPrivateTab { }.clickOpenInPrivateTab {
@ -268,9 +266,10 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
}.clickDelete { }.clickDelete {
verifyDeleteSnackBarText() verifyDeleteSnackBarText()
verifyUndoDeleteSnackBarButton() verifyUndoDeleteSnackBarButton()
@ -306,7 +305,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(defaultWebPage.url) longTapSelectItem(defaultWebPage.url)
@ -336,7 +335,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(defaultWebPage.url) longTapSelectItem(defaultWebPage.url)
@ -359,7 +358,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(defaultWebPage.url) longTapSelectItem(defaultWebPage.url)
@ -384,11 +383,12 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(firstWebPage.url) longTapSelectItem(firstWebPage.url)
longTapSelectItem(secondWebPage.url) longTapSelectItem(secondWebPage.url)
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
openActionBarOverflowOrOptionsMenu(activityTestRule.activity) openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
} }
@ -410,7 +410,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(defaultWebPage.url) longTapSelectItem(defaultWebPage.url)
@ -466,7 +466,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)

@ -21,6 +21,7 @@ import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.ui.robots.downloadRobot import org.mozilla.fenix.ui.robots.downloadRobot
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.notificationShade
import java.io.File import java.io.File
/** /**
@ -92,10 +93,7 @@ class DownloadTest {
} }
@Test @Test
@Ignore("Temp disable flakey test - see: https://github.com/mozilla-mobile/fenix/issues/5462")
fun testDownloadNotification() { fun testDownloadNotification() {
homeScreen { }.dismissOnboarding()
val defaultWebPage = TestAssetHelper.getDownloadAsset(mockWebServer) val defaultWebPage = TestAssetHelper.getDownloadAsset(mockWebServer)
navigationToolbar { navigationToolbar {
@ -108,7 +106,13 @@ class DownloadTest {
verifyDownloadPrompt() verifyDownloadPrompt()
}.clickDownload { }.clickDownload {
verifyDownloadNotificationPopup() verifyDownloadNotificationPopup()
verifyDownloadNotificationShade()
} }
mDevice.openNotification()
notificationShade {
verifySystemNotificationExists("Download completed")
}
// close notification shade before the next test
mDevice.pressBack()
} }
} }

@ -254,11 +254,9 @@ class HistoryTest {
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) { }.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openTabDrawer { }.openHomeScreen { } }.openTabDrawer {
}.openNewTab {
navigationToolbar { }.submitQuery(secondWebPage.url.toString()) {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openHistory {
longTapSelectItem(firstWebPage.url) longTapSelectItem(firstWebPage.url)

@ -57,7 +57,7 @@ class SearchTest {
}.goBack { }.goBack {
}.goBack { }.goBack {
}.openSearch { }.openSearch {
verifySearchWithText() // verifySearchWithText()
clickSearchEngineButton("DuckDuckGo") clickSearchEngineButton("DuckDuckGo")
typeSearch("mozilla") typeSearch("mozilla")
verifySearchEngineResults("DuckDuckGo") verifySearchEngineResults("DuckDuckGo")

@ -10,7 +10,6 @@ import okhttp3.mockwebserver.MockWebServer
import org.junit.Rule import org.junit.Rule
import org.junit.Before import org.junit.Before
import org.junit.After import org.junit.After
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
@ -69,7 +68,7 @@ class SettingsAboutTest {
} }
} }
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13219")
@Test @Test
fun verifyAboutFirefoxPreview() { fun verifyAboutFirefoxPreview() {
homeScreen { homeScreen {

@ -191,7 +191,8 @@ class SettingsBasicsTest {
}.enterURLAndEnterToBrowser(webpage) { }.enterURLAndEnterToBrowser(webpage) {
checkTextSizeOnWebsite(textSizePercentage, fenixApp.components) checkTextSizeOnWebsite(textSizePercentage, fenixApp.components)
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { }.openNewTab {
}.dismiss {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openAccessibilitySubMenu { }.openAccessibilitySubMenu {

@ -194,7 +194,8 @@ class SettingsPrivacyTest {
// Click save to save the login // Click save to save the login
saveLoginFromPrompt("Save") saveLoginFromPrompt("Save")
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { }.openNewTab {
}.dismiss {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
TestHelper.scrollToElementByText("Logins and passwords") TestHelper.scrollToElementByText("Logins and passwords")
@ -219,7 +220,8 @@ class SettingsPrivacyTest {
// Don't save the login, add to exceptions // Don't save the login, add to exceptions
saveLoginFromPrompt("Never save") saveLoginFromPrompt("Never save")
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { }.openNewTab {
}.dismiss {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openLoginsAndPasswordSubMenu { }.openLoginsAndPasswordSubMenu {
@ -274,7 +276,7 @@ class SettingsPrivacyTest {
browserScreen { browserScreen {
}.openTabDrawer { }.openTabDrawer {
verifyPrivateModeSelected() verifyPrivateModeSelected()
}.openHomeScreen { } }.openNewTab { }.dismiss { }
setOpenLinksInPrivateOff() setOpenLinksInPrivateOff()
@ -321,7 +323,7 @@ class SettingsPrivacyTest {
clickAddAutomaticallyButton() clickAddAutomaticallyButton()
}.openHomeScreenShortcut(pageShortcutName) { }.openHomeScreenShortcut(pageShortcutName) {
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { } }.openNewTab { }.dismiss { }
setOpenLinksInPrivateOff() setOpenLinksInPrivateOff()
restartApp(activityTestRule) restartApp(activityTestRule)
@ -331,7 +333,8 @@ class SettingsPrivacyTest {
}.searchAndOpenHomeScreenShortcut(pageShortcutName) { }.searchAndOpenHomeScreenShortcut(pageShortcutName) {
}.openTabDrawer { }.openTabDrawer {
verifyNormalModeSelected() verifyNormalModeSelected()
}.openHomeScreen { }.openNewTab {
}.dismiss {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openPrivateBrowsingSubMenu { }.openPrivateBrowsingSubMenu {

@ -4,7 +4,6 @@
package org.mozilla.fenix.ui package org.mozilla.fenix.ui
import androidx.core.net.toUri
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
@ -16,6 +15,7 @@ import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.ui.robots.clickUrlbar
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
@ -57,7 +57,8 @@ class SmokeTest {
}.goBackToWebsite { }.goBackToWebsite {
}.openTabDrawer { }.openTabDrawer {
verifyExistingTabList() verifyExistingTabList()
}.openHomeScreen { }.openNewTab {
}.dismiss {
verifyHomeScreen() verifyHomeScreen()
} }
} }
@ -105,7 +106,8 @@ class SmokeTest {
}.addToFirefoxHome { }.addToFirefoxHome {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { }.openNewTab {
}.dismiss {
verifyExistingTopSitesTabs(defaultWebPage.title) verifyExistingTopSitesTabs(defaultWebPage.title)
}.openTabDrawer { }.openTabDrawer {
}.openTab(defaultWebPage.title) { }.openTab(defaultWebPage.title) {
@ -131,13 +133,11 @@ class SmokeTest {
verifyUrl(defaultWebPage.url.toString()) verifyUrl(defaultWebPage.url.toString())
}.openTabDrawer { }.openTabDrawer {
closeTabViaXButton(defaultWebPage.title) closeTabViaXButton(defaultWebPage.title)
}.openHomeScreen { }.openNewTab {
navigationToolbar { }.submitQuery(youtubeUrl) {
}.enterURLAndEnterToBrowser(youtubeUrl.toUri()) { verifyBlueDot()
verifyBlueDot() }.openThreeDotMenu {
}.openThreeDotMenu { verifyOpenInAppButton()
verifyOpenInAppButton()
}
} }
} }
@ -184,7 +184,8 @@ class SmokeTest {
}.addToFirefoxHome { }.addToFirefoxHome {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { }.openNewTab {
}.dismiss {
togglePrivateBrowsingModeOnOff() togglePrivateBrowsingModeOnOff()
verifyExistingTopSitesTabs(defaultWebPage.title) verifyExistingTopSitesTabs(defaultWebPage.title)
togglePrivateBrowsingModeOnOff() togglePrivateBrowsingModeOnOff()
@ -208,13 +209,11 @@ class SmokeTest {
verifyUrl(defaultWebPage.url.toString()) verifyUrl(defaultWebPage.url.toString())
}.openTabDrawer { }.openTabDrawer {
closeTabViaXButton(defaultWebPage.title) closeTabViaXButton(defaultWebPage.title)
}.openHomeScreen { }.openNewTab {
navigationToolbar { }.submitQuery(youtubeUrl) {
}.enterURLAndEnterToBrowser(youtubeUrl.toUri()) { verifyBlueDot()
verifyBlueDot() }.openThreeDotMenu {
}.openThreeDotMenu { verifyOpenInAppButton()
verifyOpenInAppButton()
}
} }
} }
} }
@ -239,7 +238,8 @@ class SmokeTest {
verifyUrl("webcompat.com/issues/new") verifyUrl("webcompat.com/issues/new")
verifyTabCounter("2") verifyTabCounter("2")
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { }.openNewTab {
}.dismiss {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openEnhancedTrackingProtectionSubMenu { }.openEnhancedTrackingProtectionSubMenu {
@ -254,4 +254,60 @@ class SmokeTest {
} }
} }
} }
@Test
fun verifySearchEngineCanBeChangedTemporarilyUsingShortcuts() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openSearch {
verifyKeyboardVisibility()
clickSearchEngineButton()
verifySearchEngineList()
changeDefaultSearchEngine("Amazon.com")
verifySearchEngineIcon("Amazon.com")
}.goToSearchEngine {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabDrawer {
}.openNewTab {
clickSearchEngineButton()
mDevice.waitForIdle()
changeDefaultSearchEngine("Bing")
verifySearchEngineIcon("Bing")
}.goToSearchEngine {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabDrawer {
}.openNewTab {
clickSearchEngineButton()
mDevice.waitForIdle()
changeDefaultSearchEngine("DuckDuckGo")
verifySearchEngineIcon("DuckDuckGo")
}.goToSearchEngine {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabDrawer {
}.openNewTab {
clickSearchEngineButton()
mDevice.waitForIdle()
changeDefaultSearchEngine("Twitter")
verifySearchEngineIcon("Twitter")
}.goToSearchEngine {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabDrawer {
}.openNewTab {
clickSearchEngineButton()
changeDefaultSearchEngine("Wikipedia")
verifySearchEngineIcon("Wikipedia")
}.goToSearchEngine {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabDrawer {
// Checking whether the next search will be with default or not
}.openNewTab {
}.goToSearchEngine {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openNavigationToolbar {
clickUrlbar {
verifyDefaultSearchEngine("Google")
}
}
}
} }

@ -149,92 +149,87 @@ class TabbedBrowsingTest {
@Test @Test
fun closeTabTest() { fun closeTabTest() {
var genericURLS = TestAssetHelper.getGenericAssets(mockWebServer) val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
genericURLS.forEachIndexed { index, element ->
navigationToolbar {
}.openNewTabAndEnterToBrowser(element.url) {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}")
verifyCloseTabsButton("Test_Page_${index + 1}")
closeTabViaXButton("Test_Page_${index + 1}")
verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO")
}
mDevice.waitForIdle() navigationToolbar {
}.openNewTabAndEnterToBrowser(genericURL.url) {
browserScreen { }.openTabDrawer {
}.openTabDrawer { verifyExistingOpenTabs("Test_Page_1")
verifyExistingOpenTabs("Test_Page_${index + 1}") verifyCloseTabsButton("Test_Page_1")
swipeTabRight("Test_Page_${index + 1}") closeTabViaXButton("Test_Page_1")
verifySnackBarText("Tab closed") verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO") snackBarButtonClick("UNDO")
} }
mDevice.waitForIdle() mDevice.waitForIdle()
browserScreen { browserScreen {
}.openTabDrawer { }.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}") verifyExistingOpenTabs("Test_Page_1")
swipeTabLeft("Test_Page_${index + 1}") swipeTabRight("Test_Page_1")
verifySnackBarText("Tab closed") verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO") snackBarButtonClick("UNDO")
} }
mDevice.waitForIdle() mDevice.waitForIdle()
browserScreen { browserScreen {
}.openTabDrawer { }.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}") verifyExistingOpenTabs("Test_Page_1")
}.openHomeScreen { swipeTabLeft("Test_Page_1")
} verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO")
} }
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
}.openNewTab {
}.dismiss { }
} }
@Test @Test
fun closePrivateTabTest() { fun closePrivateTabTest() {
var genericURLS = TestAssetHelper.getGenericAssets(mockWebServer) val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen { }.togglePrivateBrowsingMode() homeScreen { }.togglePrivateBrowsingMode()
genericURLS.forEachIndexed { index, element -> navigationToolbar {
navigationToolbar { }.openNewTabAndEnterToBrowser(genericURL.url) {
}.openNewTabAndEnterToBrowser(element.url) { }.openTabDrawer {
}.openTabDrawer { verifyExistingOpenTabs("Test_Page_1")
verifyExistingOpenTabs("Test_Page_${index + 1}") verifyCloseTabsButton("Test_Page_1")
verifyCloseTabsButton("Test_Page_${index + 1}") closeTabViaXButton("Test_Page_1")
closeTabViaXButton("Test_Page_${index + 1}") verifySnackBarText("Private tab closed")
verifySnackBarText("Private tab closed") snackBarButtonClick("UNDO")
snackBarButtonClick("UNDO") }
}
mDevice.waitForIdle() mDevice.waitForIdle()
browserScreen { browserScreen {
}.openTabDrawer { }.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}") verifyExistingOpenTabs("Test_Page_1")
swipeTabRight("Test_Page_${index + 1}") swipeTabRight("Test_Page_1")
verifySnackBarText("Private tab closed") verifySnackBarText("Private tab closed")
snackBarButtonClick("UNDO") snackBarButtonClick("UNDO")
} }
mDevice.waitForIdle() mDevice.waitForIdle()
browserScreen { browserScreen {
}.openTabDrawer { }.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}") verifyExistingOpenTabs("Test_Page_1")
swipeTabLeft("Test_Page_${index + 1}") swipeTabLeft("Test_Page_1")
verifySnackBarText("Private tab closed") verifySnackBarText("Private tab closed")
snackBarButtonClick("UNDO") snackBarButtonClick("UNDO")
} }
mDevice.waitForIdle() mDevice.waitForIdle()
browserScreen { browserScreen {
}.openTabDrawer { }.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}") verifyExistingOpenTabs("Test_Page_1")
closeTabViaXButton("Test_Page_${index + 1}")
}
} }
} }
@ -290,8 +285,8 @@ class TabbedBrowsingTest {
verifyTabTrayOverflowMenu(true) verifyTabTrayOverflowMenu(true)
verifyExistingOpenTabs(defaultWebPage.title) verifyExistingOpenTabs(defaultWebPage.title)
verifyCloseTabsButton(defaultWebPage.title) verifyCloseTabsButton(defaultWebPage.title)
}.openHomeScreen { }.openNewTab {
} }.dismiss { }
} }
@Test @Test

@ -68,13 +68,14 @@ class ThreeDotMenuMainTest {
}.openHelp { }.openHelp {
verifyHelpUrl() verifyHelpUrl()
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { }.openNewTab {
}.dismiss {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openWhatsNew { }.openWhatsNew {
verifyWhatsNewURL() verifyWhatsNewURL()
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { }.openNewTab {
} }.dismiss { }
homeScreen { homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {

@ -58,7 +58,8 @@ class TopSitesTest {
}.addToFirefoxHome { }.addToFirefoxHome {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { }.openNewTab {
}.dismiss {
verifyExistingTopSitesList() verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle) verifyExistingTopSitesTabs(defaultWebPageTitle)
} }
@ -76,13 +77,15 @@ class TopSitesTest {
}.addToFirefoxHome { }.addToFirefoxHome {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { }.openNewTab {
}.dismiss {
verifyExistingTopSitesList() verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle) verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openTopSiteTabWithTitle(title = defaultWebPageTitle) { }.openTopSiteTabWithTitle(title = defaultWebPageTitle) {
verifyUrl(defaultWebPage.url.toString().replace("http://", "")) verifyUrl(defaultWebPage.url.toString().replace("http://", ""))
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { }.openNewTab {
}.dismiss {
verifyExistingTopSitesList() verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle) verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) { }.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) {
@ -105,7 +108,8 @@ class TopSitesTest {
}.addToFirefoxHome { }.addToFirefoxHome {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { }.openNewTab {
}.dismiss {
verifyExistingTopSitesList() verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle) verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) { }.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) {
@ -127,7 +131,8 @@ class TopSitesTest {
}.addToFirefoxHome { }.addToFirefoxHome {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
}.openHomeScreen { }.openNewTab {
}.dismiss {
verifyExistingTopSitesList() verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle) verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) { }.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) {

@ -35,6 +35,7 @@ import org.hamcrest.Matchers.containsString
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
@ -52,10 +53,7 @@ class BookmarksRobot {
fun verifyBookmarkedURL(url: String) = assertBookmarkURL(url) fun verifyBookmarkedURL(url: String) = assertBookmarkURL(url)
fun verifyFolderTitle(title: String) { fun verifyFolderTitle(title: String) {
mDevice.waitNotNull( mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime)
Until.findObject(text(title)),
TestAssetHelper.waitingTime
)
assertFolderTitle(title) assertFolderTitle(title)
} }

@ -12,12 +12,14 @@ import android.net.Uri
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.BundleMatchers import androidx.test.espresso.intent.matcher.BundleMatchers
import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed

@ -37,8 +37,6 @@ class DownloadRobot {
fun verifyDownloadNotificationPopup() = assertDownloadNotificationPopup() fun verifyDownloadNotificationPopup() = assertDownloadNotificationPopup()
fun verifyDownloadNotificationShade() = assertDownloadNotificationShade()
fun verifyPhotosAppOpens() = assertPhotosOpens() fun verifyPhotosAppOpens() = assertPhotosOpens()
class Transition { class Transition {
@ -98,17 +96,6 @@ private fun assertDownloadPrompt() {
mDevice.waitNotNull(Until.findObjects(By.res("org.mozilla.fenix.debug:id/download_button"))) mDevice.waitNotNull(Until.findObjects(By.res("org.mozilla.fenix.debug:id/download_button")))
} }
private fun assertDownloadNotificationShade() {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.openNotification()
mDevice.waitNotNull(
Until.findObjects(By.text("Download completed")), TestAssetHelper.waitingTime
)
// Go home (no UIDevice closeNotification) to close notification shade
mDevice.pressHome()
}
private fun assertDownloadNotificationPopup() { private fun assertDownloadNotificationPopup() {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.waitNotNull(Until.findObjects(By.text("Open")), TestAssetHelper.waitingTime) mDevice.waitNotNull(Until.findObjects(By.text("Open")), TestAssetHelper.waitingTime)

@ -553,11 +553,11 @@ private fun assertWelcomeHeader() =
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertGetTheMostHeader() = private fun assertGetTheMostHeader() =
onView(allOf(withText("Get the most out of Firefox Preview."))) onView(allOf(withText("Start syncing bookmarks, passwords, and more with your Firefox account.")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertAccountsSignInButton() = private fun assertAccountsSignInButton() =
onView(ViewMatchers.withResourceName("turn_on_sync_button")) onView(ViewMatchers.withResourceName("fxa_sign_in_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertGetToKnowHeader() = private fun assertGetToKnowHeader() =

@ -26,6 +26,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import kotlinx.android.synthetic.main.fragment_search_dialog.view.*
import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.not
@ -239,12 +240,18 @@ fun navigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationTo
return NavigationToolbarRobot.Transition() return NavigationToolbarRobot.Transition()
} }
fun clickUrlbar(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
urlBar().click()
SearchRobot().interact()
return SearchRobot.Transition()
}
private fun assertSuggestionsAreEqualTo(suggestionSize: Int, searchTerm: String) { private fun assertSuggestionsAreEqualTo(suggestionSize: Int, searchTerm: String) {
mDevice.waitForIdle() mDevice.waitForIdle()
awesomeBar().perform(typeText(searchTerm)) awesomeBar().perform(typeText(searchTerm))
mDevice.waitForIdle() mDevice.waitForIdle()
onView(withId(R.id.awesomeBar)).check(suggestionsAreEqualTo(suggestionSize)) onView(withId(R.id.awesome_bar)).check(suggestionsAreEqualTo(suggestionSize))
} }
private fun assertSuggestionsAreMoreThan(suggestionSize: Int, searchTerm: String) { private fun assertSuggestionsAreMoreThan(suggestionSize: Int, searchTerm: String) {
@ -252,7 +259,7 @@ private fun assertSuggestionsAreMoreThan(suggestionSize: Int, searchTerm: String
awesomeBar().perform(typeText(searchTerm)) awesomeBar().perform(typeText(searchTerm))
mDevice.waitForIdle() mDevice.waitForIdle()
onView(withId(R.id.awesomeBar)).check(suggestionsAreGreaterThan(suggestionSize)) onView(withId(R.id.awesome_bar)).check(suggestionsAreGreaterThan(suggestionSize))
} }
private fun assertNoHistoryBookmarks() { private fun assertNoHistoryBookmarks() {

@ -1,11 +1,12 @@
package org.mozilla.fenix.ui.robots package org.mozilla.fenix.ui.robots
import android.content.res.Resources
import androidx.test.uiautomator.By.text import androidx.test.uiautomator.By.text
import androidx.test.uiautomator.UiObjectNotFoundException
import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
@ -17,23 +18,21 @@ class NotificationRobot {
UiSelector().resourceId("com.android.systemui:id/notification_stack_scroller") UiSelector().resourceId("com.android.systemui:id/notification_stack_scroller")
) )
mDevice.waitNotNull( val notificationFound: Boolean
Until.hasObject(text(notificationMessage)),
waitingTime notificationFound = try {
) notificationTray().getChildByText(
UiSelector().text(notificationMessage), notificationMessage, true
).exists()
} catch (e: UiObjectNotFoundException) {
false
}
var notificationFound = false if (!notificationFound) {
while (!notificationFound) { // swipe 2 times to expand the silent notifications on API 28 and higher, single-swipe doesn't do it
try { notificationTray().swipeUp(2)
val notification = notificationTray().getChildByText( val notification = mDevice.findObject(UiSelector().textContains(notificationMessage))
UiSelector().text(notificationMessage), notificationMessage, assertTrue(notification.exists())
true
)
notification.exists()
notificationFound = true
} catch (e: Resources.NotFoundException) {
e.printStackTrace()
}
} }
} }

@ -6,6 +6,7 @@
package org.mozilla.fenix.ui.robots package org.mozilla.fenix.ui.robots
import android.widget.ToggleButton
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction import androidx.test.espresso.ViewInteraction
@ -16,7 +17,9 @@ import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
@ -28,8 +31,11 @@ import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.startsWith import org.hamcrest.CoreMatchers.startsWith
import org.hamcrest.Matchers import org.hamcrest.Matchers
import org.junit.Assert.assertEquals
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
/** /**
@ -47,6 +53,24 @@ class SearchRobot {
fun verifySearchSettings() = assertSearchSettings() fun verifySearchSettings() = assertSearchSettings()
fun verifySearchBarEmpty() = assertSearchBarEmpty() fun verifySearchBarEmpty() = assertSearchBarEmpty()
fun verifyKeyboardVisibility() = assertKeyboardVisibility(isExpectedToBeVisible = true)
fun verifySearchEngineList() = assertSearchEngineList()
fun verifySearchEngineIcon(expectedText: String) {
onView(withContentDescription(expectedText))
}
fun verifyDefaultSearchEngine(expectedText: String) = assertDefaultSearchEngine(expectedText)
fun changeDefaultSearchEngine(searchEngineName: String) =
selectDefaultSearchEngine(searchEngineName)
fun clickSearchEngineButton() {
val searchEngineButton = mDevice.findObject(UiSelector()
.instance(1)
.className(ToggleButton::class.java))
searchEngineButton.waitForExists(waitingTime)
searchEngineButton.click()
}
fun clickScanButton() { fun clickScanButton() {
scanButton().perform(click()) scanButton().perform(click())
} }
@ -82,10 +106,10 @@ class SearchRobot {
fun scrollToSearchEngineSettings() { fun scrollToSearchEngineSettings() {
// Soft keyboard is visible on screen on view access; hide it // Soft keyboard is visible on screen on view access; hide it
onView(allOf(withId(R.id.search_layout))).perform( onView(allOf(withId(R.id.search_wrapper))).perform(
closeSoftKeyboard() closeSoftKeyboard()
) )
onView(allOf(withId(R.id.awesomeBar))).perform(ViewActions.swipeUp()) onView(allOf(withId(R.id.awesome_bar))).perform(ViewActions.swipeUp())
} }
fun clickSearchEngineSettings() { fun clickSearchEngineSettings() {
@ -99,6 +123,13 @@ class SearchRobot {
class Transition { class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
fun dismiss(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
mDevice.waitForIdle()
mDevice.pressBack()
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
fun openBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun openBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitForIdle() mDevice.waitForIdle()
browserToolbarEditView().perform(typeText("mozilla\n")) browserToolbarEditView().perform(typeText("mozilla\n"))
@ -106,10 +137,23 @@ class SearchRobot {
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
fun submitQuery(query: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitForIdle()
browserToolbarEditView().perform(typeText(query + "\n"))
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun goToSearchEngine(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
NavigationToolbarRobot().interact()
return NavigationToolbarRobot.Transition()
}
} }
} }
private fun awesomeBar() = onView(withId(R.id.awesomeBar)) private fun awesomeBar() = onView(withId(R.id.awesome_bar))
private fun browserToolbarEditView() = private fun browserToolbarEditView() =
onView(Matchers.allOf(withId(R.id.mozac_browser_toolbar_edit_url_view))) onView(Matchers.allOf(withId(R.id.mozac_browser_toolbar_edit_url_view)))
@ -136,6 +180,8 @@ private fun scanButton(): ViewInteraction {
private fun clearButton() = onView(withId(R.id.mozac_browser_toolbar_clear_view)) private fun clearButton() = onView(withId(R.id.mozac_browser_toolbar_clear_view))
private fun searchWrapper() = onView(withId(R.id.search_wrapper))
private fun assertSearchEngineURL(searchEngineName: String) { private fun assertSearchEngineURL(searchEngineName: String) {
mDevice.waitNotNull( mDevice.waitNotNull(
Until.findObject(By.textContains("${searchEngineName.toLowerCase()}.com/?q=mozilla")), Until.findObject(By.textContains("${searchEngineName.toLowerCase()}.com/?q=mozilla")),
@ -178,4 +224,46 @@ fun searchScreen(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
return SearchRobot.Transition() return SearchRobot.Transition()
} }
private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) = {
mDevice.waitNotNull(
Until.findObject(
By.text("Search Engine")
), waitingTime
)
assertEquals(
isExpectedToBeVisible,
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
.executeShellCommand("dumpsys input_method | grep mInputShown")
.contains("mInputShown=true")
)
}
private fun assertSearchEngineList() {
onView(withId(R.id.mozac_browser_toolbar_edit_icon)).click()
onView(withText("Google"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("Amazon.com"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("Bing"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("DuckDuckGo"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("Twitter"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("Wikipedia"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun selectDefaultSearchEngine(searchEngine: String) {
onView(withId(R.id.mozac_browser_toolbar_edit_icon)).click()
onView(withText(searchEngine))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.perform(click())
}
private fun assertDefaultSearchEngine(expectedText: String) {
onView(allOf(withId(R.id.mozac_browser_toolbar_edit_icon), withContentDescription(expectedText)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun goBackButton() = onView(allOf(withContentDescription("Navigate up"))) private fun goBackButton() = onView(allOf(withContentDescription("Navigate up")))

@ -222,7 +222,7 @@ private fun assertLibrariesUsed() {
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click()) .perform(click())
onView(withId(R.id.action_bar)).check(matches(hasDescendant(withText(containsString("Firefox Preview | OSS Libraries"))))) onView(withId(R.id.navigationToolbar)).check(matches(hasDescendant(withText(containsString("Firefox Preview | OSS Libraries")))))
Espresso.pressBack() Espresso.pressBack()
} }

@ -135,12 +135,12 @@ class TabDrawerRobot {
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
fun openHomeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { fun openNewTab(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
mDevice.waitForIdle() mDevice.waitForIdle()
newTabButton().perform(click()) newTabButton().perform(click())
HomeScreenRobot().interact() SearchRobot().interact()
return HomeScreenRobot.Transition() return SearchRobot.Transition()
} }
fun toggleToNormalTabs(interact: TabDrawerRobot.() -> Unit): Transition { fun toggleToNormalTabs(interact: TabDrawerRobot.() -> Unit): Transition {

@ -164,7 +164,7 @@ class ThreeDotMenuMainRobot {
fun openBookmarks(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition { fun openBookmarks(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime) mDevice.findObject(UiSelector().resourceId("R.id.bookmark_list")).waitForExists(waitingTime)
bookmarksButton().click() bookmarksButton().click()
BookmarksRobot().interact() BookmarksRobot().interact()

@ -215,7 +215,8 @@
android:name=".crashes.CrashListActivity" android:name=".crashes.CrashListActivity"
android:exported="false" /> android:exported="false" />
<activity android:name=".widget.VoiceSearchActivity" /> <activity android:name=".widget.VoiceSearchActivity"
android:theme="@style/Theme.AppCompat.Translucent"/>
<activity <activity
android:name=".settings.account.AuthCustomTabActivity" android:name=".settings.account.AuthCustomTabActivity"

@ -31,5 +31,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromEditCustomSearchEngineFragment(R.id.editCustomSearchEngineFragment), FromEditCustomSearchEngineFragment(R.id.editCustomSearchEngineFragment),
FromAddonDetailsFragment(R.id.addonDetailsFragment), FromAddonDetailsFragment(R.id.addonDetailsFragment),
FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment), FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment),
FromLoginDetailFragment(R.id.loginDetailFragment) FromLoginDetailFragment(R.id.loginDetailFragment),
FromTabTray(R.id.tabTrayDialogFragment)
} }

@ -15,42 +15,44 @@ object FeatureFlags {
const val pullToRefreshEnabled = false const val pullToRefreshEnabled = false
/** /**
* Allows edit of saved logins. * Shows Synced Tabs in the tabs tray.
*
* Tracking issue: https://github.com/mozilla-mobile/fenix/issues/13892
*/ */
const val loginsEdit = true val syncedTabsInTabsTray = Config.channel.isNightlyOrDebug
/** /**
* Enable tab sync feature * Enables viewing tab history
*/ */
const val syncedTabs = true val tabHistory = Config.channel.isNightlyOrDebug
/** /**
* Enables new tab tray pref * Enables the new search experience
*/ */
val tabTray = Config.channel.isNightlyOrDebug const val newSearchExperience = true
/** /**
* Enables gestures on the browser chrome that depend on a [SwipeGestureLayout] * Enables showing the top frequently visited sites
*/ */
val browserChromeGestures = Config.channel.isNightlyOrDebug val topFrecentSite = Config.channel.isNightlyOrDebug
/** /**
* Enables viewing tab history * Enables wait til first contentful paint
*/ */
val tabHistory = Config.channel.isNightlyOrDebug val waitUntilPaintToDraw = true // Just enables the setting in Secret Settings
/** /**
* Enables the new search experience * Enables downloads with external download managers.
*/ */
val newSearchExperience = true // Just enables the setting in Secret Settings val externalDownloadManager = true // Just enables the setting in Secret Settings
/** /**
* Enables wait til first contentful paint * Enables viewing downloads in browser.
*/ */
val waitUntilPaintToDraw = true // Just enables the setting in secret settings val viewDownloads = Config.channel.isNightlyOrDebug
/** /**
* Enables downloads with external download managers. * Enables selecting from multiple logins.
*/ */
val externalDownloadManager = true val loginSelect = Config.channel.isNightlyOrDebug
} }

@ -22,6 +22,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import mozilla.appservices.Megazord import mozilla.appservices.Megazord
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.state.action.SystemAction
import mozilla.components.concept.push.PushProcessor import mozilla.components.concept.push.PushProcessor
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
import mozilla.components.lib.crash.CrashReporter import mozilla.components.lib.crash.CrashReporter
@ -42,6 +43,7 @@ import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.fenix.StrictModeManager.enableStrictMode import org.mozilla.fenix.StrictModeManager.enableStrictMode
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.MetricServiceType import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.resetPoliciesAfter import org.mozilla.fenix.ext.resetPoliciesAfter
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StorageStatsMetrics import org.mozilla.fenix.perf.StorageStatsMetrics
@ -141,7 +143,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
} }
} }
prefetchForHomeFragment()
setupLeakCanary() setupLeakCanary()
startMetricsIfEnabled() startMetricsIfEnabled()
setupPush() setupPush()
@ -156,18 +157,20 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
// } // }
initVisualCompletenessQueueAndQueueTasks() initVisualCompletenessQueueAndQueueTasks()
components.appStartupTelemetry.onFenixApplicationOnCreate()
} }
private fun initVisualCompletenessQueueAndQueueTasks() { private fun initVisualCompletenessQueueAndQueueTasks() {
val taskQueue = components.performance.visualCompletenessQueue val queue = components.performance.visualCompletenessQueue.queue
fun initQueue() { fun initQueue() {
registerActivityLifecycleCallbacks(PerformanceActivityLifecycleCallbacks(taskQueue)) registerActivityLifecycleCallbacks(PerformanceActivityLifecycleCallbacks(queue))
} }
fun queueInitExperiments() { fun queueInitExperiments() {
if (settings().isExperimentationEnabled) { if (settings().isExperimentationEnabled) {
taskQueue.runIfReadyOrQueue { queue.runIfReadyOrQueue {
Experiments.initialize( Experiments.initialize(
applicationContext = applicationContext, applicationContext = applicationContext,
onExperimentsUpdated = { onExperimentsUpdated = {
@ -188,7 +191,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
} }
fun queueInitStorageAndServices() { fun queueInitStorageAndServices() {
components.performance.visualCompletenessQueue.runIfReadyOrQueue { components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
logger.info("Running post-visual completeness tasks...") logger.info("Running post-visual completeness tasks...")
logElapsedTime(logger, "Storage initialization") { logElapsedTime(logger, "Storage initialization") {
@ -208,7 +211,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
fun queueMetrics() { fun queueMetrics() {
if (SDK_INT >= Build.VERSION_CODES.O) { // required by StorageStatsMetrics. if (SDK_INT >= Build.VERSION_CODES.O) { // required by StorageStatsMetrics.
taskQueue.runIfReadyOrQueue { queue.runIfReadyOrQueue {
// Because it may be slow to capture the storage stats, it might be preferred to // Because it may be slow to capture the storage stats, it might be preferred to
// create a WorkManager task for this metric, however, I ran out of // create a WorkManager task for this metric, however, I ran out of
// implementation time and WorkManager is harder to test. // implementation time and WorkManager is harder to test.
@ -217,6 +220,12 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
} }
} }
fun queueReviewPrompt() {
GlobalScope.launch(Dispatchers.IO) {
components.reviewPromptController.trackApplicationLaunch()
}
}
initQueue() initQueue()
// We init these items in the visual completeness queue to avoid them initing in the critical // We init these items in the visual completeness queue to avoid them initing in the critical
@ -224,6 +233,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
queueInitExperiments() queueInitExperiments()
queueInitStorageAndServices() queueInitStorageAndServices()
queueMetrics() queueMetrics()
queueReviewPrompt()
} }
private fun startMetricsIfEnabled() { private fun startMetricsIfEnabled() {
@ -257,14 +267,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
// no-op, LeakCanary is disabled by default // no-op, LeakCanary is disabled by default
} }
// This is for issue https://github.com/mozilla-mobile/fenix/issues/11660. We prefetch our info for startup
// so that we're sure that we have all the data available as our fragment is launched.
private fun prefetchForHomeFragment() {
StrictMode.allowThreadDiskReads().resetPoliciesAfter {
components.core.topSiteStorage.prefetch()
}
}
private fun setupPush() { private fun setupPush() {
// Sets the PushFeature as the singleton instance for push messages to go to. // Sets the PushFeature as the singleton instance for push messages to go to.
// We need the push feature setup here to deliver messages in the case where the service // We need the push feature setup here to deliver messages in the case where the service
@ -318,7 +320,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
runOnlyInMainProcess { runOnlyInMainProcess {
components.core.icons.onTrimMemory(level) components.core.icons.onTrimMemory(level)
components.core.sessionManager.onTrimMemory(level) components.core.store.dispatch(SystemAction.LowMemoryAction(level))
} }
} }

@ -44,7 +44,7 @@ enum class GlobalDirections(val navDirections: NavDirections, val destinationId:
R.id.deleteBrowsingDataFragment R.id.deleteBrowsingDataFragment
), ),
SettingsAddonManager( SettingsAddonManager(
NavGraphDirections.actionGlobalSettingsAddonsManagementFragment(), NavGraphDirections.actionGlobalAddonsManagementFragment(),
R.id.addonsManagementFragment R.id.addonsManagementFragment
), ),
SettingsLogins( SettingsLogins(

@ -6,6 +6,7 @@ package org.mozilla.fenix
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.StrictMode import android.os.StrictMode
@ -21,7 +22,6 @@ import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PROTECTED import androidx.annotation.VisibleForTesting.PROTECTED
import androidx.appcompat.app.ActionBar import androidx.appcompat.app.ActionBar
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.doOnPreDraw
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
@ -40,13 +40,11 @@ import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.WebExtensionState import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
import mozilla.components.feature.privatemode.notification.PrivateNotificationFeature import mozilla.components.feature.privatemode.notification.PrivateNotificationFeature
import mozilla.components.feature.search.BrowserStoreSearchAdapter import mozilla.components.feature.search.BrowserStoreSearchAdapter
import mozilla.components.feature.search.SearchAdapter
import mozilla.components.service.fxa.sync.SyncReason import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.android.arch.lifecycle.addObservers import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
@ -56,7 +54,6 @@ import mozilla.components.support.ktx.android.content.share
import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.support.ktx.kotlin.isUrl
import mozilla.components.support.ktx.kotlin.toNormalizedUrl import mozilla.components.support.ktx.kotlin.toNormalizedUrl
import mozilla.components.support.locale.LocaleAwareAppCompatActivity import mozilla.components.support.locale.LocaleAwareAppCompatActivity
import mozilla.components.support.utils.RunWhenReadyQueue
import mozilla.components.support.utils.SafeIntent import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent import mozilla.components.support.utils.toSafeIntent
import mozilla.components.support.webextensions.WebExtensionPopupFeature import mozilla.components.support.webextensions.WebExtensionPopupFeature
@ -71,6 +68,7 @@ import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.ext.alreadyOnDestination import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
@ -100,9 +98,11 @@ import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirection
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
import org.mozilla.fenix.tabtray.TabTrayDialogFragment import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.BrowsersCache
import java.lang.ref.WeakReference
/** /**
* The main activity of the application. The application is primarily a single Activity (this one) * The main activity of the application. The application is primarily a single Activity (this one)
@ -121,7 +121,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
private var isVisuallyComplete = false private var isVisuallyComplete = false
private var visualCompletenessQueue: RunWhenReadyQueue? = null
private var privateNotificationObserver: PrivateNotificationFeature<PrivateNotificationService>? = null private var privateNotificationObserver: PrivateNotificationFeature<PrivateNotificationService>? = null
private var isToolbarInflated = false private var isToolbarInflated = false
@ -156,6 +155,16 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
message = "onCreate()",
data = mapOf(
"recreated" to (savedInstanceState != null).toString(),
"intent" to (intent?.action ?: "null")
)
)
components.publicSuffixList.prefetch() components.publicSuffixList.prefetch()
setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent)) setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent))
@ -163,13 +172,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// Must be after we set the content view // Must be after we set the content view
if (isVisuallyComplete) { if (isVisuallyComplete) {
rootContainer.doOnPreDraw { components.performance.visualCompletenessQueue
// This delay is temporary. We are delaying 5 seconds until the performance .attachViewToRunVisualCompletenessQueueLater(WeakReference(rootContainer))
// team can locate the real point of visual completeness.
it.postDelayed({
visualCompletenessQueue!!.ready()
}, delay)
}
} }
sessionObserver = UriOpenedObserver(this) sessionObserver = UriOpenedObserver(this)
@ -226,19 +230,33 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
captureSnapshotTelemetryMetrics() captureSnapshotTelemetryMetrics()
setAppAllStartTelemetry(intent.toSafeIntent()) startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null)
StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE. StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
} }
protected open fun setAppAllStartTelemetry(safeIntent: SafeIntent) { protected open fun startupTelemetryOnCreateCalled(safeIntent: SafeIntent, hasSavedInstanceState: Boolean) {
components.appAllSourceStartTelemetry.receivedIntentInHomeActivity(safeIntent) components.appStartupTelemetry.onHomeActivityOnCreate(safeIntent, hasSavedInstanceState)
}
override fun onRestart() {
super.onRestart()
components.appStartupTelemetry.onHomeActivityOnRestart()
} }
@CallSuper @CallSuper
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
message = "onResume()"
)
components.appStartupTelemetry.onHomeActivityOnResume()
components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue { components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
lifecycleScope.launch { lifecycleScope.launch {
// Make sure accountManager is initialized. // Make sure accountManager is initialized.
@ -266,6 +284,29 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
} }
override fun onStart() {
super.onStart()
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
message = "onStart()"
)
}
override fun onStop() {
super.onStop()
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
message = "onStop()",
data = mapOf(
"finishing" to isFinishing.toString()
)
)
}
final override fun onPause() { final override fun onPause() {
if (settings().lastKnownMode.isPrivate) { if (settings().lastKnownMode.isPrivate) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
@ -273,6 +314,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
super.onPause() super.onPause()
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
message = "onPause()",
data = mapOf(
"finishing" to isFinishing.toString()
)
)
// Every time the application goes into the background, it is possible that the user // Every time the application goes into the background, it is possible that the user
// is about to change the browsers installed on their system. Therefore, we reset the cache of // is about to change the browsers installed on their system. Therefore, we reset the cache of
// all the installed browsers. // all the installed browsers.
@ -283,9 +333,39 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
message = "onDestroy()",
data = mapOf(
"finishing" to isFinishing.toString()
)
)
privateNotificationObserver?.stop() privateNotificationObserver?.stop()
} }
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
message = "onConfigurationChanged()"
)
}
override fun recreate() {
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
message = "recreate()"
)
super.recreate()
}
/** /**
* Handles intents received when the activity is open. * Handles intents received when the activity is open.
*/ */
@ -293,6 +373,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
super.onNewIntent(intent) super.onNewIntent(intent)
intent ?: return intent ?: return
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
message = "onNewIntent()",
data = mapOf(
"intent" to intent.action.toString()
)
)
val intentProcessors = val intentProcessors =
listOf(CrashReporterIntentProcessor()) + externalSourceIntentProcessors listOf(CrashReporterIntentProcessor()) + externalSourceIntentProcessors
val intentHandled = val intentHandled =
@ -317,7 +406,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
.let(::getIntentAllSource) .let(::getIntentAllSource)
?.also { components.analytics.metrics.track(Event.AppReceivedIntent(it)) } ?.also { components.analytics.metrics.track(Event.AppReceivedIntent(it)) }
setAppAllStartTelemetry(intent.toSafeIntent()) components.appStartupTelemetry.onHomeActivityOnNewIntent(intent.toSafeIntent())
} }
/** /**
@ -331,7 +420,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
): View? = when (name) { ): View? = when (name) {
EngineView::class.java.name -> components.core.engine.createView(context, attrs).apply { EngineView::class.java.name -> components.core.engine.createView(context, attrs).apply {
selectionActionDelegate = DefaultSelectionActionDelegate( selectionActionDelegate = DefaultSelectionActionDelegate(
getSearchAdapter(components.core.store), BrowserStoreSearchAdapter(
components.core.store,
tabId = getIntentSessionId(intent.toSafeIntent())
),
resources = context.resources, resources = context.resources,
shareTextClicked = { share(it) }, shareTextClicked = { share(it) },
emailTextClicked = { email(it) }, emailTextClicked = { email(it) },
@ -424,9 +516,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
super.onUserLeaveHint() super.onUserLeaveHint()
} }
protected open fun getSearchAdapter(store: BrowserStore): SearchAdapter =
BrowserStoreSearchAdapter(store)
protected open fun getBreadcrumbMessage(destination: NavDestination): String { protected open fun getBreadcrumbMessage(destination: NavDestination): String {
val fragmentName = resources.getResourceEntryName(destination.id) val fragmentName = resources.getResourceEntryName(destination.id)
return "Changing to fragment $fragmentName, isCustomTab: false" return "Changing to fragment $fragmentName, isCustomTab: false"
@ -597,6 +686,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId) AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromLoginDetailFragment -> BrowserDirection.FromLoginDetailFragment ->
LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId) LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTray ->
TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
} }
/** /**
@ -675,9 +766,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
* The root container is null at this point, so let the HomeActivity know that * The root container is null at this point, so let the HomeActivity know that
* we are visually complete. * we are visually complete.
*/ */
fun postVisualCompletenessQueue(visualCompletenessQueue: RunWhenReadyQueue) { fun setVisualCompletenessQueueReady() {
isVisuallyComplete = true isVisuallyComplete = true
this.visualCompletenessQueue = visualCompletenessQueue
} }
private fun captureSnapshotTelemetryMetrics() = CoroutineScope(Dispatchers.IO).launch { private fun captureSnapshotTelemetryMetrics() = CoroutineScope(Dispatchers.IO).launch {
@ -714,7 +804,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
const val PRIVATE_BROWSING_MODE = "private_browsing_mode" const val PRIVATE_BROWSING_MODE = "private_browsing_mode"
const val EXTRA_DELETE_PRIVATE_TABS = "notification_delete_and_open" const val EXTRA_DELETE_PRIVATE_TABS = "notification_delete_and_open"
const val EXTRA_OPENED_FROM_NOTIFICATION = "notification_open" const val EXTRA_OPENED_FROM_NOTIFICATION = "notification_open"
const val delay = 5000L
const val START_IN_RECENTS_SCREEN = "start_in_recents_screen" const val START_IN_RECENTS_SCREEN = "start_in_recents_screen"
// PWA must have been used within last 30 days to be considered "recently used" for the // PWA must have been used within last 30 days to be considered "recently used" for the

@ -66,6 +66,12 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
} }
} }
override fun onDestroyView() {
super.onDestroyView()
// letting go of the resources to avoid memory leak.
adapter = null
}
private fun bindRecyclerView(view: View) { private fun bindRecyclerView(view: View) {
val managementView = AddonsManagementView( val managementView = AddonsManagementView(
navController = findNavController(), navController = findNavController(),
@ -120,7 +126,6 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
addonNameTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context), addonNameTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context),
addonSummaryTextColor = ThemeManager.resolveAttribute(R.attr.secondaryText, context), addonSummaryTextColor = ThemeManager.resolveAttribute(R.attr.secondaryText, context),
sectionsTypeFace = ResourcesCompat.getFont(context, R.font.metropolis_semibold), sectionsTypeFace = ResourcesCompat.getFont(context, R.font.metropolis_semibold),
addonBackgroundIconColor = ThemeManager.resolveAttribute(R.attr.inset, requireContext()),
addonAllowPrivateBrowsingLabelDrawableRes = R.drawable.ic_add_on_private_browsing_label addonAllowPrivateBrowsingLabelDrawableRes = R.drawable.ic_add_on_private_browsing_label
) )
} }

@ -42,6 +42,7 @@ import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.accounts.FxaCapability import mozilla.components.feature.accounts.FxaCapability
import mozilla.components.feature.accounts.FxaWebChannelFeature import mozilla.components.feature.accounts.FxaWebChannelFeature
@ -56,10 +57,10 @@ import mozilla.components.feature.privatemode.feature.SecureWindowFeature
import mozilla.components.feature.prompts.PromptFeature import mozilla.components.feature.prompts.PromptFeature
import mozilla.components.feature.prompts.share.ShareDelegate import mozilla.components.feature.prompts.share.ShareDelegate
import mozilla.components.feature.readerview.ReaderViewFeature import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.feature.search.SearchFeature
import mozilla.components.feature.session.FullScreenFeature import mozilla.components.feature.session.FullScreenFeature
import mozilla.components.feature.session.PictureInPictureFeature import mozilla.components.feature.session.PictureInPictureFeature
import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.session.SessionFeature
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.session.SwipeRefreshFeature import mozilla.components.feature.session.SwipeRefreshFeature
import mozilla.components.feature.session.behavior.EngineViewBottomBehavior import mozilla.components.feature.session.behavior.EngineViewBottomBehavior
import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.feature.sitepermissions.SitePermissions
@ -98,6 +99,7 @@ import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.downloads.DynamicDownloadDialog import org.mozilla.fenix.downloads.DynamicDownloadDialog
import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.accessibilityManager import org.mozilla.fenix.ext.accessibilityManager
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.enterToImmersiveMode import org.mozilla.fenix.ext.enterToImmersiveMode
import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.getPreferenceKey
@ -133,6 +135,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
get() = _browserToolbarView!! get() = _browserToolbarView!!
protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>() protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
protected val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>() private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>() private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>()
@ -150,6 +153,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>() private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
private var fullScreenMediaFeature = private var fullScreenMediaFeature =
ViewBoundFeatureWrapper<MediaFullscreenOrientationFeature>() ViewBoundFeatureWrapper<MediaFullscreenOrientationFeature>()
private val searchFeature = ViewBoundFeatureWrapper<SearchFeature>()
private var pipFeature: PictureInPictureFeature? = null private var pipFeature: PictureInPictureFeature? = null
var customTabSessionId: String? = null var customTabSessionId: String? = null
@ -169,11 +173,16 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
require(arguments != null) require(arguments != null)
customTabSessionId = arguments?.getString(EXTRA_SESSION_ID) customTabSessionId = arguments?.getString(EXTRA_SESSION_ID)
val view = if (FeatureFlags.browserChromeGestures) { // Diagnostic breadcrumb for "Display already aquired" crash:
inflater.inflate(R.layout.browser_gesture_wrapper, container, false) // https://github.com/mozilla-mobile/android-components/issues/7960
} else { breadcrumb(
inflater.inflate(R.layout.fragment_browser, container, false) message = "onCreateView()",
} data = mapOf(
"customTabSessionId" to customTabSessionId.toString()
)
)
val view = inflater.inflate(R.layout.fragment_browser, container, false)
val activity = activity as HomeActivity val activity = activity as HomeActivity
activity.themeManager.applyStatusBarTheme(activity) activity.themeManager.applyStatusBarTheme(activity)
@ -212,6 +221,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
} }
return getSessionById()?.also { session -> return getSessionById()?.also { session ->
val openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply {
action = Intent.ACTION_VIEW
putExtra(HomeActivity.OPEN_TO_BROWSER, true)
}
val browserToolbarController = DefaultBrowserToolbarController( val browserToolbarController = DefaultBrowserToolbarController(
activity = requireActivity() as HomeActivity, activity = requireActivity() as HomeActivity,
navController = findNavController(), navController = findNavController(),
@ -227,15 +241,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
swipeRefresh = swipeRefresh, swipeRefresh = swipeRefresh,
browserAnimator = browserAnimator, browserAnimator = browserAnimator,
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply { openInFenixIntent = openInFenixIntent,
action = Intent.ACTION_VIEW
putExtra(HomeActivity.OPEN_TO_BROWSER, true)
},
bookmarkTapped = { viewLifecycleOwner.lifecycleScope.launch { bookmarkTapped(it) } }, bookmarkTapped = { viewLifecycleOwner.lifecycleScope.launch { bookmarkTapped(it) } },
scope = viewLifecycleOwner.lifecycleScope, scope = viewLifecycleOwner.lifecycleScope,
tabCollectionStorage = requireComponents.core.tabCollectionStorage, tabCollectionStorage = requireComponents.core.tabCollectionStorage,
topSiteStorage = requireComponents.core.topSiteStorage,
onTabCounterClicked = { onTabCounterClicked = {
thumbnailsFeature.get()?.requestScreenshot()
findNavController().nav( findNavController().nav(
R.id.browserFragment, R.id.browserFragment,
BrowserFragmentDirections.actionGlobalTabTrayDialogFragment() BrowserFragmentDirections.actionGlobalTabTrayDialogFragment()
@ -477,7 +488,16 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
}, },
onNeedToRequestPermissions = { permissions -> onNeedToRequestPermissions = { permissions ->
requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS) requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS)
}), },
loginPickerView = if (FeatureFlags.loginSelect) loginSelectBar else null,
onManageLogins = {
browserAnimator.captureEngineViewAndDrawStatically {
val directions =
NavGraphDirections.actionGlobalSavedLoginsAuthFragment()
findNavController().navigate(directions)
}
}
),
owner = this, owner = this,
view = view view = view
) )
@ -486,7 +506,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
feature = SessionFeature( feature = SessionFeature(
requireComponents.core.store, requireComponents.core.store,
requireComponents.useCases.sessionUseCases.goBack, requireComponents.useCases.sessionUseCases.goBack,
requireComponents.useCases.engineSessionUseCases,
view.engineView, view.engineView,
customTabSessionId customTabSessionId
), ),
@ -494,6 +513,26 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
view = view view = view
) )
searchFeature.set(
feature = SearchFeature(store, customTabSessionId) { request, tabId ->
val parentSession = sessionManager.findSessionById(tabId)
val useCase = if (request.isPrivate) {
requireComponents.useCases.searchUseCases.newPrivateTabSearch
} else {
requireComponents.useCases.searchUseCases.newTabSearch
}
if (parentSession?.isCustomTabSession() == true) {
useCase.invoke(request.query)
requireActivity().startActivity(openInFenixIntent)
} else {
useCase.invoke(request.query, parentSession = parentSession)
}
},
owner = this,
view = view
)
val accentHighContrastColor = val accentHighContrastColor =
ThemeManager.resolveAttribute(R.attr.accentHighContrast, context) ThemeManager.resolveAttribute(R.attr.accentHighContrast, context)
@ -541,7 +580,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
fullScreenFeature.set( fullScreenFeature.set(
feature = FullScreenFeature( feature = FullScreenFeature(
requireComponents.core.store, requireComponents.core.store,
SessionUseCases(sessionManager), requireComponents.useCases.sessionUseCases,
customTabSessionId, customTabSessionId,
::viewportFitChange, ::viewportFitChange,
::fullScreenChanged ::fullScreenChanged
@ -593,7 +632,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
if (showEngineView) { if (showEngineView) {
engineView?.asView()?.isVisible = true engineView?.asView()?.isVisible = true
swipeRefresh.alpha = 1f swipeRefresh?.alpha = 1f
} else { } else {
engineView?.asView()?.isVisible = false engineView?.asView()?.isVisible = false
} }
@ -1063,11 +1102,38 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
*/ */
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
message = "onDestroyView()"
)
requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this) requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this)
_browserToolbarView = null _browserToolbarView = null
_browserInteractor = null _browserInteractor = null
} }
override fun onAttach(context: Context) {
super.onAttach(context)
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
message = "onAttach()"
)
}
override fun onDetach() {
super.onDetach()
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
message = "onDetach()"
)
}
companion object { companion object {
private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id" private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id"
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1 private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1

@ -15,7 +15,6 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.browser_gesture_wrapper.*
import kotlinx.android.synthetic.main.fragment_browser.* import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.android.synthetic.main.fragment_browser.view.* import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -26,13 +25,11 @@ import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.feature.app.links.AppLinksUseCases import mozilla.components.feature.app.links.AppLinksUseCases
import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.readerview.ReaderViewFeature import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.feature.search.SearchFeature
import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.WindowFeature import mozilla.components.feature.tabs.WindowFeature
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.addons.runIfFragmentIsAttached import org.mozilla.fenix.addons.runIfFragmentIsAttached
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
@ -55,8 +52,6 @@ import org.mozilla.fenix.trackingprotection.TrackingProtectionOverlay
class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>() private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val searchFeature = ViewBoundFeatureWrapper<SearchFeature>()
private val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
private var readerModeAvailable = false private var readerModeAvailable = false
@ -77,19 +72,15 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
val components = context.components val components = context.components
return super.initializeUI(view)?.also { return super.initializeUI(view)?.also {
// We need to wrap this whole thing in an if here because gestureLayout will not exist gestureLayout.addGestureListener(
// if the feature flag is off ToolbarGestureHandler(
if (FeatureFlags.browserChromeGestures) { activity = requireActivity(),
gestureLayout.addGestureListener( contentLayout = browserLayout,
ToolbarGestureHandler( tabPreview = tabPreview,
activity = requireActivity(), toolbarLayout = browserToolbarView.view,
contentLayout = browserLayout, sessionManager = components.core.sessionManager
tabPreview = tabPreview,
toolbarLayout = browserToolbarView.view,
sessionManager = components.core.sessionManager
)
) )
} )
val readerModeAction = val readerModeAction =
BrowserToolbar.ToggleButton( BrowserToolbar.ToggleButton(
@ -148,23 +139,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
owner = this, owner = this,
view = view view = view
) )
searchFeature.set(
feature = SearchFeature(components.core.store) {
if (it.isPrivate) {
components.useCases.searchUseCases.newPrivateTabSearch.invoke(
it.query,
parentSession = getSessionById()
)
} else {
components.useCases.searchUseCases.newTabSearch.invoke(
it.query,
parentSession = getSessionById()
)
}
},
owner = this,
view = view
)
} }
} }

@ -6,19 +6,19 @@ package org.mozilla.fenix.browser
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.app.Activity import android.app.Activity
import android.graphics.PointF import android.graphics.PointF
import android.graphics.Rect import android.graphics.Rect
import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewConfiguration import android.view.ViewConfiguration
import androidx.annotation.Dimension import androidx.annotation.Dimension
import androidx.annotation.Dimension.DP import androidx.annotation.Dimension.DP
import androidx.core.animation.doOnEnd
import androidx.core.graphics.contains import androidx.core.graphics.contains
import androidx.core.graphics.toPoint import androidx.core.graphics.toPoint
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.dynamicanimation.animation.DynamicAnimation import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import androidx.dynamicanimation.animation.FlingAnimation
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.support.ktx.android.util.dpToPx
@ -61,11 +61,6 @@ class ToolbarGestureHandler(
private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop
private val minimumFlingVelocity = ViewConfiguration.get(activity).scaledMinimumFlingVelocity private val minimumFlingVelocity = ViewConfiguration.get(activity).scaledMinimumFlingVelocity
private val defaultVelocity = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
MINIMUM_ANIMATION_VELOCITY,
activity.resources.displayMetrics
)
private var gestureDirection = GestureDirection.LEFT_TO_RIGHT private var gestureDirection = GestureDirection.LEFT_TO_RIGHT
@ -143,25 +138,12 @@ class ToolbarGestureHandler(
) { ) {
val destination = getDestination() val destination = getDestination()
if (destination is Destination.Tab && isGestureComplete(velocityX)) { if (destination is Destination.Tab && isGestureComplete(velocityX)) {
animateToNextTab(velocityX, destination.session) animateToNextTab(destination.session)
} else { } else {
animateCanceledGesture(velocityX) animateCanceledGesture(velocityX)
} }
} }
private fun createFlingAnimation(
view: View,
minValue: Float,
maxValue: Float,
startVelocity: Float
): FlingAnimation =
FlingAnimation(view, DynamicAnimation.TRANSLATION_X).apply {
setMinValue(minValue)
setMaxValue(maxValue)
setStartVelocity(startVelocity)
friction = ViewConfiguration.getScrollFriction()
}
private fun getDestination(): Destination { private fun getDestination(): Destination {
val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
val currentSession = sessionManager.selectedSession ?: return Destination.None val currentSession = sessionManager.selectedSession ?: return Destination.None
@ -234,73 +216,59 @@ class ToolbarGestureHandler(
abs(velocityX) >= minimumFlingVelocity) abs(velocityX) >= minimumFlingVelocity)
} }
private fun getVelocityFromFling(velocityX: Float): Float { private fun getAnimator(finalContextX: Float, duration: Long): ValueAnimator {
return max(abs(velocityX), defaultVelocity) return ValueAnimator.ofFloat(contentLayout.translationX, finalContextX).apply {
this.duration = duration
this.interpolator = LinearOutSlowInInterpolator()
addUpdateListener { animator ->
val value = animator.animatedValue as Float
contentLayout.translationX = value
tabPreview.translationX = when (gestureDirection) {
GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset
GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset
}
}
}
} }
private fun animateToNextTab(velocityX: Float, session: Session) { private fun animateToNextTab(session: Session) {
val browserFinalXCoordinate: Float = when (gestureDirection) { val browserFinalXCoordinate: Float = when (gestureDirection) {
GestureDirection.RIGHT_TO_LEFT -> -windowWidth.toFloat() - previewOffset GestureDirection.RIGHT_TO_LEFT -> -windowWidth.toFloat() - previewOffset
GestureDirection.LEFT_TO_RIGHT -> windowWidth.toFloat() + previewOffset GestureDirection.LEFT_TO_RIGHT -> windowWidth.toFloat() + previewOffset
} }
val animationVelocity = when (gestureDirection) {
GestureDirection.RIGHT_TO_LEFT -> -getVelocityFromFling(velocityX)
GestureDirection.LEFT_TO_RIGHT -> getVelocityFromFling(velocityX)
}
// Finish animating the contentLayout off screen and tabPreview on screen // Finish animating the contentLayout off screen and tabPreview on screen
createFlingAnimation( getAnimator(browserFinalXCoordinate, FINISHED_GESTURE_ANIMATION_DURATION).apply {
view = contentLayout, doOnEnd {
minValue = min(0f, browserFinalXCoordinate), contentLayout.translationX = 0f
maxValue = max(0f, browserFinalXCoordinate), sessionManager.select(session)
startVelocity = animationVelocity
).addUpdateListener { _, value, _ -> // Fade out the tab preview to prevent flickering
tabPreview.translationX = when (gestureDirection) { val shortAnimationDuration =
GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset activity.resources.getInteger(android.R.integer.config_shortAnimTime)
GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset tabPreview.animate()
.alpha(0f)
.setDuration(shortAnimationDuration.toLong())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
tabPreview.isVisible = false
}
})
} }
}.addEndListener { _, _, _, _ ->
contentLayout.translationX = 0f
sessionManager.select(session)
// Fade out the tab preview to prevent flickering
val shortAnimationDuration =
activity.resources.getInteger(android.R.integer.config_shortAnimTime)
tabPreview.animate()
.alpha(0f)
.setDuration(shortAnimationDuration.toLong())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
tabPreview.isVisible = false
}
})
}.start() }.start()
} }
private fun animateCanceledGesture(gestureVelocity: Float) { private fun animateCanceledGesture(velocityX: Float) {
val velocity = if (getDestination() is Destination.None) { val duration = if (abs(velocityX) >= minimumFlingVelocity) {
defaultVelocity CANCELED_FLING_ANIMATION_DURATION
} else { } else {
getVelocityFromFling(gestureVelocity) CANCELED_GESTURE_ANIMATION_DURATION
}.let { v ->
when (gestureDirection) {
GestureDirection.RIGHT_TO_LEFT -> v
GestureDirection.LEFT_TO_RIGHT -> -v
}
} }
createFlingAnimation( getAnimator(0f, duration).apply {
view = contentLayout, doOnEnd {
minValue = min(0f, contentLayout.translationX), tabPreview.isVisible = false
maxValue = max(0f, contentLayout.translationX),
startVelocity = velocity
).addUpdateListener { _, value, _ ->
tabPreview.translationX = when (gestureDirection) {
GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset
GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset
} }
}.addEndListener { _, _, _, _ ->
tabPreview.isVisible = false
}.start() }.start()
} }
@ -337,15 +305,24 @@ class ToolbarGestureHandler(
private const val OVERSCROLL_HIDE_PERCENT = 0.20 private const val OVERSCROLL_HIDE_PERCENT = 0.20
/** /**
* The speed of the fling animation (in dp per second). * The size of the gap between the tab preview and content layout.
*/ */
@Dimension(unit = DP) @Dimension(unit = DP)
private const val MINIMUM_ANIMATION_VELOCITY = 1500f private const val PREVIEW_OFFSET = 48
/** /**
* The size of the gap between the tab preview and content layout. * Animation duration when switching to another tab
*/ */
@Dimension(unit = DP) private const val FINISHED_GESTURE_ANIMATION_DURATION = 250L
private const val PREVIEW_OFFSET = 48
/**
* Animation duration gesture is canceled due to the swipe not being far enough
*/
private const val CANCELED_GESTURE_ANIMATION_DURATION = 200L
/**
* Animation duration gesture is canceled due to a swipe in the opposite direction
*/
private const val CANCELED_FLING_ANIMATION_DURATION = 150L
} }
} }

@ -32,7 +32,6 @@ import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider
import mozilla.components.service.sync.logins.SyncableLoginsStorage import mozilla.components.service.sync.logins.SyncableLoginsStorage
import mozilla.components.support.utils.RunWhenReadyQueue import mozilla.components.support.utils.RunWhenReadyQueue
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
@ -85,11 +84,8 @@ class BackgroundServices(
) )
@VisibleForTesting @VisibleForTesting
val supportedEngines = if (FeatureFlags.syncedTabs) { val supportedEngines =
setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords, SyncEngine.Tabs) setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords, SyncEngine.Tabs)
} else {
setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords)
}
private val syncConfig = SyncConfig(supportedEngines, syncPeriodInMinutes = 240L) // four hours private val syncConfig = SyncConfig(supportedEngines, syncPeriodInMinutes = 240L) // four hours
init { init {
@ -98,10 +94,7 @@ class BackgroundServices(
GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage) GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarkStorage) GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarkStorage)
GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to passwordsStorage) GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to passwordsStorage)
GlobalSyncableStoreProvider.configureStore(SyncEngine.Tabs to remoteTabsStorage)
if (FeatureFlags.syncedTabs) {
GlobalSyncableStoreProvider.configureStore(SyncEngine.Tabs to remoteTabsStorage)
}
} }
private val telemetryAccountObserver = TelemetryAccountObserver( private val telemetryAccountObserver = TelemetryAccountObserver(

@ -18,7 +18,7 @@ import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.migration.state.MigrationStore import mozilla.components.support.migration.state.MigrationStore
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.components.metrics.AppAllSourceStartTelemetry import org.mozilla.fenix.components.metrics.AppStartupTelemetry
import org.mozilla.fenix.utils.ClipboardHandler import org.mozilla.fenix.utils.ClipboardHandler
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@ -44,7 +44,7 @@ class Components(private val context: Context) {
) )
} }
val services by lazy { Services(context, backgroundServices.accountManager) } val services by lazy { Services(context, backgroundServices.accountManager) }
val core by lazy { Core(context) } val core by lazy { Core(context, analytics.crashReporter) }
val search by lazy { Search(context) } val search by lazy { Search(context) }
val useCases by lazy { val useCases by lazy {
UseCases( UseCases(
@ -53,7 +53,8 @@ class Components(private val context: Context) {
core.sessionManager, core.sessionManager,
core.store, core.store,
search.searchEngineManager, search.searchEngineManager,
core.webAppShortcutManager core.webAppShortcutManager,
core.topSiteStorage
) )
} }
val intentProcessors by lazy { val intentProcessors by lazy {
@ -83,7 +84,7 @@ class Components(private val context: Context) {
} }
} }
val appAllSourceStartTelemetry by lazy { AppAllSourceStartTelemetry(analytics.metrics) } val appStartupTelemetry by lazy { AppStartupTelemetry(analytics.metrics) }
@Suppress("MagicNumber") @Suppress("MagicNumber")
val addonUpdater by lazy { val addonUpdater by lazy {
@ -114,4 +115,11 @@ class Components(private val context: Context) {
val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context.getSystemService()!!) } val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context.getSystemService()!!) }
val settings by lazy { Settings(context) } val settings by lazy { Settings(context) }
val reviewPromptController by lazy {
ReviewPromptController(
context,
FenixReviewSettings(settings)
)
}
} }

@ -7,6 +7,7 @@ package org.mozilla.fenix.components
import GeckoProvider import GeckoProvider
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.StrictMode
import io.sentry.Sentry import io.sentry.Sentry
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -15,7 +16,9 @@ import kotlinx.coroutines.withContext
import mozilla.components.browser.engine.gecko.GeckoEngine import mozilla.components.browser.engine.gecko.GeckoEngine
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
import mozilla.components.browser.icons.BrowserIcons import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.engine.EngineMiddleware
import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
@ -37,6 +40,8 @@ import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.WebAppShortcutManager import mozilla.components.feature.pwa.WebAppShortcutManager
import mozilla.components.feature.readerview.ReaderViewMiddleware import mozilla.components.feature.readerview.ReaderViewMiddleware
import mozilla.components.feature.session.HistoryDelegate import mozilla.components.feature.session.HistoryDelegate
import mozilla.components.feature.top.sites.DefaultTopSitesStorage
import mozilla.components.feature.top.sites.PinnedSiteStorage
import mozilla.components.feature.webcompat.WebCompatFeature import mozilla.components.feature.webcompat.WebCompatFeature
import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature
import mozilla.components.feature.webnotifications.WebNotificationFeature import mozilla.components.feature.webnotifications.WebNotificationFeature
@ -46,16 +51,21 @@ import mozilla.components.service.digitalassetlinks.RelationChecker
import mozilla.components.service.digitalassetlinks.local.StatementApi import mozilla.components.service.digitalassetlinks.local.StatementApi
import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
import mozilla.components.service.sync.logins.SyncableLoginsStorage import mozilla.components.service.sync.logins.SyncableLoginsStorage
import mozilla.components.support.base.crash.CrashReporting
import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.AppRequestInterceptor import org.mozilla.fenix.AppRequestInterceptor
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.resetPoliciesAfter
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.media.MediaService import org.mozilla.fenix.media.MediaService
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.advanced.getSelectedLocale
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -63,7 +73,7 @@ import java.util.concurrent.TimeUnit
* Component group for all core browser functionality. * Component group for all core browser functionality.
*/ */
@Mockable @Mockable
class Core(private val context: Context) { class Core(private val context: Context, private val crashReporter: CrashReporting) {
/** /**
* The browser engine component initialized based on the build * The browser engine component initialized based on the build
* configuration (see build variants). * configuration (see build variants).
@ -134,10 +144,14 @@ class Core(private val context: Context) {
DownloadMiddleware(context, DownloadService::class.java), DownloadMiddleware(context, DownloadService::class.java),
ReaderViewMiddleware(), ReaderViewMiddleware(),
ThumbnailsMiddleware(thumbnailStorage) ThumbnailsMiddleware(thumbnailStorage)
) ) + EngineMiddleware.create(engine, ::findSessionById)
) )
} }
private fun findSessionById(tabId: String): Session? {
return sessionManager.findSessionById(tabId)
}
/** /**
* The [CustomTabsServiceStore] holds global custom tabs related data. * The [CustomTabsServiceStore] holds global custom tabs related data.
*/ */
@ -184,7 +198,7 @@ class Core(private val context: Context) {
// Now that we have restored our previous state (if there's one) let's setup auto saving the state while // Now that we have restored our previous state (if there's one) let's setup auto saving the state while
// the app is used. // the app is used.
sessionStorage.autoSave(sessionManager) sessionStorage.autoSave(store)
.periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS) .periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
.whenGoingToBackground() .whenGoingToBackground()
.whenSessionsChange() .whenSessionsChange()
@ -228,7 +242,7 @@ class Core(private val context: Context) {
// Use these for startup-path code, where we don't want to do any work that's not strictly necessary. // Use these for startup-path code, where we don't want to do any work that's not strictly necessary.
// For example, this is how the GeckoEngine delegates (history, logins) are configured. // For example, this is how the GeckoEngine delegates (history, logins) are configured.
// We can fully initialize GeckoEngine without initialized our storage. // We can fully initialize GeckoEngine without initialized our storage.
val lazyHistoryStorage = lazy { PlacesHistoryStorage(context) } val lazyHistoryStorage = lazy { PlacesHistoryStorage(context, crashReporter) }
val lazyBookmarksStorage = lazy { PlacesBookmarksStorage(context) } val lazyBookmarksStorage = lazy { PlacesBookmarksStorage(context) }
val lazyPasswordsStorage = lazy { SyncableLoginsStorage(context, passwordsEncryptionKey) } val lazyPasswordsStorage = lazy { SyncableLoginsStorage(context, passwordsEncryptionKey) }
@ -249,7 +263,46 @@ class Core(private val context: Context) {
*/ */
val thumbnailStorage by lazy { ThumbnailStorage(context) } val thumbnailStorage by lazy { ThumbnailStorage(context) }
val topSiteStorage by lazy { TopSiteStorage(context) } val pinnedSiteStorage by lazy { PinnedSiteStorage(context) }
val topSiteStorage by lazy {
val defaultTopSites = mutableListOf<Pair<String, String>>()
StrictMode.allowThreadDiskReads().resetPoliciesAfter {
if (!context.settings().defaultTopSitesAdded) {
defaultTopSites.add(
Pair(
context.getString(R.string.default_top_site_google),
SupportUtils.GOOGLE_URL
)
)
if (LocaleManager.getSelectedLocale(context).language == "en") {
defaultTopSites.add(
Pair(
context.getString(R.string.pocket_pinned_top_articles),
SupportUtils.POCKET_TRENDING_URL
)
)
}
defaultTopSites.add(
Pair(
context.getString(R.string.default_top_site_wikipedia),
SupportUtils.WIKIPEDIA_URL
)
)
context.settings().defaultTopSitesAdded = true
}
}
DefaultTopSitesStorage(
pinnedSiteStorage,
historyStorage,
defaultTopSites
)
}
val permissionStorage by lazy { PermissionStorage(context) } val permissionStorage by lazy { PermissionStorage(context) }
@ -273,7 +326,8 @@ class Core(private val context: Context) {
getSecureAbove22Preferences().getString(PASSWORDS_KEY) getSecureAbove22Preferences().getString(PASSWORDS_KEY)
?: generateEncryptionKey(KEY_STRENGTH).also { ?: generateEncryptionKey(KEY_STRENGTH).also {
if (context.settings().passwordsEncryptionKeyGenerated && if (context.settings().passwordsEncryptionKeyGenerated &&
isSentryEnabled()) { isSentryEnabled()
) {
// We already had previously generated an encryption key, but we have lost it // We already had previously generated an encryption key, but we have lost it
Sentry.capture("Passwords encryption key for passwords storage was lost and we generated a new one") Sentry.capture("Passwords encryption key for passwords storage was lost and we generated a new one")
} }
@ -290,7 +344,7 @@ class Core(private val context: Context) {
fun getPreferredColorScheme(): PreferredColorScheme { fun getPreferredColorScheme(): PreferredColorScheme {
val inDark = val inDark =
(context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES Configuration.UI_MODE_NIGHT_YES
return when { return when {
context.settings().shouldUseDarkTheme -> PreferredColorScheme.Dark context.settings().shouldUseDarkTheme -> PreferredColorScheme.Dark
context.settings().shouldUseLightTheme -> PreferredColorScheme.Light context.settings().shouldUseLightTheme -> PreferredColorScheme.Light

@ -12,6 +12,7 @@ import android.widget.FrameLayout
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.ContentFrameLayout import androidx.appcompat.widget.ContentFrameLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.updatePadding
import androidx.core.widget.TextViewCompat import androidx.core.widget.TextViewCompat
import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.ContentViewCallback import com.google.android.material.snackbar.ContentViewCallback
@ -124,11 +125,8 @@ class FenixSnackbar private constructor(
return FenixSnackbar(parent, content, callback, isError).also { return FenixSnackbar(parent, content, callback, isError).also {
it.duration = durationOrAccessibleDuration it.duration = durationOrAccessibleDuration
it.view.setPadding( it.view.updatePadding(
0, bottom = if (
0,
0,
if (
isDisplayedWithBrowserToolbar && isDisplayedWithBrowserToolbar &&
shouldUseBottomToolbar && shouldUseBottomToolbar &&
// If the view passed in is a ContentFrameLayout, it does not matter // If the view passed in is a ContentFrameLayout, it does not matter

@ -5,10 +5,11 @@
package org.mozilla.fenix.components package org.mozilla.fenix.components
import mozilla.components.support.utils.RunWhenReadyQueue import mozilla.components.support.utils.RunWhenReadyQueue
import org.mozilla.fenix.perf.VisualCompletenessQueue
/** /**
* Component group for all functionality related to performance. * Component group for all functionality related to performance.
*/ */
class PerformanceComponent { class PerformanceComponent {
val visualCompletenessQueue by lazy { RunWhenReadyQueue() } val visualCompletenessQueue by lazy { VisualCompletenessQueue(RunWhenReadyQueue()) }
} }

@ -0,0 +1,99 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components
import android.app.Activity
import android.content.Context
import androidx.annotation.VisibleForTesting
import com.google.android.play.core.ktx.launchReview
import com.google.android.play.core.ktx.requestReview
import com.google.android.play.core.review.ReviewManagerFactory
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.withContext
import org.mozilla.fenix.utils.Settings
/**
* Interface that describes the settings needed to track the Review Prompt.
*/
interface ReviewSettings {
var numberOfAppLaunches: Int
val isDefaultBrowser: Boolean
var lastReviewPromptTimeInMillis: Long
}
/**
* Wraps `Settings` to conform to `ReviewSettings`.
*/
class FenixReviewSettings(
val settings: Settings
) : ReviewSettings {
override var numberOfAppLaunches: Int
get() = settings.numberOfAppLaunches
set(value) { settings.numberOfAppLaunches = value }
override val isDefaultBrowser: Boolean
get() = settings.isDefaultBrowser()
override var lastReviewPromptTimeInMillis: Long
get() = settings.lastReviewPromptTimeInMillis
set(value) { settings.lastReviewPromptTimeInMillis = value }
}
/**
* Controls the Review Prompt behavior.
*/
class ReviewPromptController(
private val context: Context,
private val reviewSettings: ReviewSettings,
private val timeNowInMillis: () -> Long = { System.currentTimeMillis() },
private val tryPromptReview: suspend (Activity) -> Unit = {
val manager = ReviewManagerFactory.create(context)
val reviewInfo = manager.requestReview()
withContext(Main) {
manager.launchReview(it, reviewInfo)
}
}
) {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@Volatile var reviewPromptIsReady = false
suspend fun promptReview(activity: Activity) {
if (shouldShowPrompt()) {
tryPromptReview(activity)
reviewSettings.lastReviewPromptTimeInMillis = timeNowInMillis()
}
}
fun trackApplicationLaunch() {
reviewSettings.numberOfAppLaunches = reviewSettings.numberOfAppLaunches + 1
// We only want to show the the prompt after we've finished "launching" the application.
reviewPromptIsReady = true
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun shouldShowPrompt(): Boolean {
if (!reviewPromptIsReady) {
return false
} else {
// We only want to try to show it once to avoid unnecessary disk reads
reviewPromptIsReady = false
}
if (!reviewSettings.isDefaultBrowser) { return false }
val hasOpenedFiveTimes = reviewSettings.numberOfAppLaunches >= NUMBER_OF_LAUNCHES_REQUIRED
val now = timeNowInMillis()
val apprxFourMonthsAgo = now - (APPRX_MONTH_IN_MILLIS * NUMBER_OF_MONTHS_TO_PASS)
val lastPrompt = reviewSettings.lastReviewPromptTimeInMillis
val hasNotBeenPromptedLastFourMonths = lastPrompt == 0L || lastPrompt <= apprxFourMonthsAgo
return hasOpenedFiveTimes && hasNotBeenPromptedLastFourMonths
}
companion object {
private const val APPRX_MONTH_IN_MILLIS: Long = 1000L * 60L * 60L * 24L * 30L
private const val NUMBER_OF_LAUNCHES_REQUIRED = 5
private const val NUMBER_OF_MONTHS_TO_PASS = 4
}
}

@ -1,96 +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.components
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.feature.top.sites.TopSiteStorage
import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.observeOnce
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.advanced.getSelectedLocale
import org.mozilla.fenix.utils.Mockable
@Mockable
class TopSiteStorage(private val context: Context) {
var cachedTopSites = listOf<TopSite>()
val storage by lazy {
TopSiteStorage(context)
}
init {
addDefaultTopSites()
}
/**
* Adds a new [TopSite].
*/
fun addTopSite(title: String, url: String, isDefault: Boolean = false) {
storage.addTopSite(title, url, isDefault)
}
/**
* Returns a [LiveData] list of all the [TopSite] instances.
*/
fun getTopSites(): LiveData<List<TopSite>> {
return storage.getTopSites().asLiveData()
}
/**
* Removes the given [TopSite].
*/
fun removeTopSite(topSite: TopSite) {
storage.removeTopSite(topSite)
}
private fun addDefaultTopSites() {
val topSiteCandidates = mutableListOf<Pair<String, String>>()
if (!context.settings().defaultTopSitesAdded) {
topSiteCandidates.add(
Pair(
context.getString(R.string.default_top_site_google),
SupportUtils.GOOGLE_URL
)
)
if (LocaleManager.getSelectedLocale(context).language == "en") {
topSiteCandidates.add(
Pair(
context.getString(R.string.pocket_pinned_top_articles),
SupportUtils.POCKET_TRENDING_URL
)
)
}
topSiteCandidates.add(
Pair(
context.getString(R.string.default_top_site_wikipedia),
SupportUtils.WIKIPEDIA_URL
)
)
GlobalScope.launch(Dispatchers.IO) {
topSiteCandidates.forEach { (title, url) ->
addTopSite(title, url, isDefault = true)
}
}
context.settings().defaultTopSitesAdded = true
}
}
fun prefetch() {
getTopSites().observeOnce {
cachedTopSites = it
}
}
}

@ -7,7 +7,6 @@ package org.mozilla.fenix.components
import android.content.Context import android.content.Context
import mozilla.components.browser.search.SearchEngineManager import mozilla.components.browser.search.SearchEngineManager
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.usecases.EngineSessionUseCases
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.Engine
import mozilla.components.feature.app.links.AppLinksUseCases import mozilla.components.feature.app.links.AppLinksUseCases
@ -20,6 +19,8 @@ import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.session.SettingsUseCases import mozilla.components.feature.session.SettingsUseCases
import mozilla.components.feature.session.TrackingProtectionUseCases import mozilla.components.feature.session.TrackingProtectionUseCases
import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.TopSitesStorage
import mozilla.components.feature.top.sites.TopSitesUseCases
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
/** /**
@ -34,12 +35,13 @@ class UseCases(
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val store: BrowserStore, private val store: BrowserStore,
private val searchEngineManager: SearchEngineManager, private val searchEngineManager: SearchEngineManager,
private val shortcutManager: WebAppShortcutManager private val shortcutManager: WebAppShortcutManager,
private val topSitesStorage: TopSitesStorage
) { ) {
/** /**
* Use cases that provide engine interactions for a given browser session. * Use cases that provide engine interactions for a given browser session.
*/ */
val sessionUseCases by lazy { SessionUseCases(sessionManager) } val sessionUseCases by lazy { SessionUseCases(store, sessionManager) }
/** /**
* Use cases that provide tab management. * Use cases that provide tab management.
@ -49,7 +51,7 @@ class UseCases(
/** /**
* Use cases that provide search engine integration. * Use cases that provide search engine integration.
*/ */
val searchUseCases by lazy { SearchUseCases(context, searchEngineManager, sessionManager) } val searchUseCases by lazy { SearchUseCases(context, store, searchEngineManager, sessionManager) }
/** /**
* Use cases that provide settings management. * Use cases that provide settings management.
@ -66,7 +68,10 @@ class UseCases(
val contextMenuUseCases by lazy { ContextMenuUseCases(store) } val contextMenuUseCases by lazy { ContextMenuUseCases(store) }
val engineSessionUseCases by lazy { EngineSessionUseCases(sessionManager) }
val trackingProtectionUseCases by lazy { TrackingProtectionUseCases(store, engine) } val trackingProtectionUseCases by lazy { TrackingProtectionUseCases(store, engine) }
/**
* Use cases that provide top sites management.
*/
val topSitesUseCase by lazy { TopSitesUseCases(topSitesStorage) }
} }

@ -1,59 +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.components.metrics
import android.content.Intent
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import mozilla.components.support.utils.SafeIntent
/**
* Tracks how the application was opened through [Event.AppOpenedAllSourceStartup].
* We only considered to be "opened" if it received an intent and the app was in the background.
*/
class AppAllSourceStartTelemetry(private val metrics: MetricController) : LifecycleObserver {
// default value is true to capture the first launch of the application
private var wasApplicationInBackground = true
init {
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}
fun receivedIntentInExternalAppBrowserActivity(safeIntent: SafeIntent) {
setAppOpenedAllSourceFromIntent(safeIntent, true)
}
fun receivedIntentInHomeActivity(safeIntent: SafeIntent) {
setAppOpenedAllSourceFromIntent(safeIntent, false)
}
private fun setAppOpenedAllSourceFromIntent(intent: SafeIntent, isExternalAppBrowserActivity: Boolean) {
if (!wasApplicationInBackground) {
return
}
val source = when {
isExternalAppBrowserActivity -> Event.AppOpenedAllSourceStartup.Source.CUSTOM_TAB
intent.isLauncherIntent -> Event.AppOpenedAllSourceStartup.Source.APP_ICON
intent.action == Intent.ACTION_VIEW -> Event.AppOpenedAllSourceStartup.Source.LINK
else -> Event.AppOpenedAllSourceStartup.Source.UNKNOWN
}
metrics.track(Event.AppOpenedAllSourceStartup(source))
wasApplicationInBackground = false
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
@VisibleForTesting(otherwise = PRIVATE)
fun onApplicationOnStop() {
wasApplicationInBackground = true
}
}

@ -0,0 +1,146 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.metrics
import android.content.Intent
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import mozilla.components.support.utils.SafeIntent
import org.mozilla.fenix.components.metrics.Event.AppAllStartup
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.APP_ICON
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.CUSTOM_TAB
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.LINK
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.UNKNOWN
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.ERROR
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.HOT
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.COLD
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.WARM
/**
* Tracks application startup source, type, and whether or not activity has savedInstance to restore
* the activity from. Sample metric = [source = COLD, type = APP_ICON, hasSavedInstance = false]
* The basic idea is to collect these metrics from different phases of startup through
* [AppAllStartup] and finally report them on Activity's onResume() function.
*/
@Suppress("TooManyFunctions")
class AppStartupTelemetry(private val metrics: MetricController) : LifecycleObserver {
init {
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}
private var isMetricRecordedSinceAppWasForegrounded = false
private var wasAppCreateCalledBeforeActivityCreate = false
private var onCreateData: AppAllStartup? = null
private var onRestartData: Pair<Type, Boolean?>? = null
private var onNewIntentData: Source? = null
fun onFenixApplicationOnCreate() {
wasAppCreateCalledBeforeActivityCreate = true
}
fun onHomeActivityOnCreate(safeIntent: SafeIntent, hasSavedInstanceState: Boolean) {
setOnCreateData(safeIntent, hasSavedInstanceState, false)
}
fun onExternalAppBrowserOnCreate(safeIntent: SafeIntent, hasSavedInstanceState: Boolean) {
setOnCreateData(safeIntent, hasSavedInstanceState, true)
}
fun onHomeActivityOnRestart() {
// we are not setting [Source] in this method since source is derived from an intent.
// therefore source gets set in onNewIntent().
onRestartData = Pair(HOT, null)
}
fun onHomeActivityOnNewIntent(safeIntent: SafeIntent) {
// we are only setting [Source] in this method since source is derived from an intent].
// other metric fields are set in onRestart()
onNewIntentData = getStartupSourceFromIntent(safeIntent, false)
}
private fun setOnCreateData(
safeIntent: SafeIntent,
hasSavedInstanceState: Boolean,
isExternalAppBrowserActivity: Boolean
) {
onCreateData = AppAllStartup(
getStartupSourceFromIntent(safeIntent, isExternalAppBrowserActivity),
getAppStartupType(),
hasSavedInstanceState
)
wasAppCreateCalledBeforeActivityCreate = false
}
private fun getAppStartupType(): Type {
return if (wasAppCreateCalledBeforeActivityCreate) COLD else WARM
}
private fun getStartupSourceFromIntent(
intent: SafeIntent,
isExternalAppBrowserActivity: Boolean
): Source {
return when {
// since the intent action is same (ACTION_VIEW) for both CUSTOM_TAB and LINK.
// we have to make sure that we are checking for CUSTOM_TAB condition first as this
// check does not rely on intent action
isExternalAppBrowserActivity -> CUSTOM_TAB
intent.isLauncherIntent -> APP_ICON
intent.action == Intent.ACTION_VIEW -> LINK
// one of the unknown case is app switcher, where we go to the recent tasks to launch
// Fenix.
else -> UNKNOWN
}
}
/**
* The reason we record metric on resume is because we need to wait for onNewIntent(), and
* we are not guaranteed that onNewIntent() will be called before or after onStart() / onRestart().
* However we are guaranteed onResume() will be called after onNewIntent() and onStart(). Source:
* https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent)
*/
fun onHomeActivityOnResume() {
recordMetric()
}
private fun recordMetric() {
if (!isMetricRecordedSinceAppWasForegrounded) {
val appAllStartup: AppAllStartup = if (onCreateData != null) {
onCreateData!!
} else {
mergeOnRestartAndOnNewIntentIntoStartup()
}
metrics.track(appAllStartup)
isMetricRecordedSinceAppWasForegrounded = true
}
// we don't want any weird previous states to persist on our next metric record.
onCreateData = null
onNewIntentData = null
onRestartData = null
}
private fun mergeOnRestartAndOnNewIntentIntoStartup(): AppAllStartup {
return AppAllStartup(
onNewIntentData ?: UNKNOWN,
onRestartData?.first ?: ERROR,
onRestartData?.second
)
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun onApplicationOnStop() {
// application was backgrounded, we need to record the new metric type if
// application was to come to foreground again.
// Therefore we set the isMetricRecorded flag to false.
isMetricRecordedSinceAppWasForegrounded = false
}
}

@ -142,9 +142,6 @@ sealed class Event {
object WhatsNewTapped : Event() object WhatsNewTapped : Event()
object SupportTapped : Event() object SupportTapped : Event()
object PrivacyNoticeTapped : Event() object PrivacyNoticeTapped : Event()
object RightsTapped : Event()
object LicensingTapped : Event()
object LibrariesThatWeUseTapped : Event()
object PocketTopSiteClicked : Event() object PocketTopSiteClicked : Event()
object PocketTopSiteRemoved : Event() object PocketTopSiteRemoved : Event()
object FennecToFenixMigrated : Event() object FennecToFenixMigrated : Event()
@ -319,11 +316,28 @@ sealed class Event {
get() = hashMapOf(Events.appOpenedAllStartupKeys.source to source.name) get() = hashMapOf(Events.appOpenedAllStartupKeys.source to source.name)
} }
data class AppOpenedAllSourceStartup(val source: Source) : Event() { data class AppAllStartup(
val source: Source,
val type: Type,
val hasSavedInstanceState: Boolean? = null
) : Event() {
enum class Source { APP_ICON, LINK, CUSTOM_TAB, UNKNOWN } enum class Source { APP_ICON, LINK, CUSTOM_TAB, UNKNOWN }
enum class Type { COLD, WARM, HOT, ERROR }
override val extras: Map<Events.appOpenedAllStartupKeys, String>? override val extras: Map<Events.appOpenedAllStartupKeys, String>?
get() = hashMapOf(Events.appOpenedAllStartupKeys.source to source.name) get() {
val extrasMap = hashMapOf(
Events.appOpenedAllStartupKeys.source to source.toString(),
Events.appOpenedAllStartupKeys.type to type.toString()
)
// we are only sending hasSavedInstanceState whenever we get data from
// activity's oncreate() method.
if (hasSavedInstanceState != null) {
extrasMap[Events.appOpenedAllStartupKeys.hasSavedInstanceState] =
hasSavedInstanceState.toString()
}
return extrasMap
}
} }
data class CollectionSaveButtonPressed(val fromScreen: String) : Event() { data class CollectionSaveButtonPressed(val fromScreen: String) : Event() {
@ -488,7 +502,7 @@ sealed class Event {
NEW_PRIVATE_TAB, SHARE, BACK, FORWARD, RELOAD, STOP, OPEN_IN_FENIX, NEW_PRIVATE_TAB, SHARE, BACK, FORWARD, RELOAD, STOP, OPEN_IN_FENIX,
SAVE_TO_COLLECTION, ADD_TO_TOP_SITES, ADD_TO_HOMESCREEN, QUIT, READER_MODE_ON, SAVE_TO_COLLECTION, ADD_TO_TOP_SITES, ADD_TO_HOMESCREEN, QUIT, READER_MODE_ON,
READER_MODE_OFF, OPEN_IN_APP, BOOKMARK, READER_MODE_APPEARANCE, ADDONS_MANAGER, READER_MODE_OFF, OPEN_IN_APP, BOOKMARK, READER_MODE_APPEARANCE, ADDONS_MANAGER,
BOOKMARKS, HISTORY, SYNC_TABS BOOKMARKS, HISTORY, SYNC_TABS, DOWNLOADS
} }
override val extras: Map<Events.browserMenuActionKeys, String>? override val extras: Map<Events.browserMenuActionKeys, String>?

@ -106,7 +106,7 @@ private val Event.wrapper: EventWrapper<*>?
{ Events.appReceivedIntent.record(it) }, { Events.appReceivedIntent.record(it) },
{ Events.appReceivedIntentKeys.valueOf(it) } { Events.appReceivedIntentKeys.valueOf(it) }
) )
is Event.AppOpenedAllSourceStartup -> EventWrapper( is Event.AppAllStartup -> EventWrapper(
{ Events.appOpenedAllStartup.record(it) }, { Events.appOpenedAllStartup.record(it) },
{ Events.appOpenedAllStartupKeys.valueOf(it) } { Events.appOpenedAllStartupKeys.valueOf(it) }
) )
@ -531,15 +531,6 @@ private val Event.wrapper: EventWrapper<*>?
is Event.PrivacyNoticeTapped -> EventWrapper<NoExtraKeys>( is Event.PrivacyNoticeTapped -> EventWrapper<NoExtraKeys>(
{ AboutPage.privacyNoticeTapped.record(it) } { AboutPage.privacyNoticeTapped.record(it) }
) )
is Event.RightsTapped -> EventWrapper<NoExtraKeys>(
{ AboutPage.rightsTapped.record(it) }
)
is Event.LicensingTapped -> EventWrapper<NoExtraKeys>(
{ AboutPage.licensingTapped.record(it) }
)
is Event.LibrariesThatWeUseTapped -> EventWrapper<NoExtraKeys>(
{ AboutPage.librariesTapped.record(it) }
)
is Event.PocketTopSiteClicked -> EventWrapper<NoExtraKeys>( is Event.PocketTopSiteClicked -> EventWrapper<NoExtraKeys>(
{ Pocket.pocketTopSiteClicked.record(it) } { Pocket.pocketTopSiteClicked.record(it) }
) )
@ -725,7 +716,7 @@ class GleanMetricsService(private val context: Context) : MetricsService {
// The code below doesn't need to execute immediately, so we'll add them to the visual // The code below doesn't need to execute immediately, so we'll add them to the visual
// completeness task queue to be run later. // completeness task queue to be run later.
context.components.performance.visualCompletenessQueue.runIfReadyOrQueue { context.components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue {
// We have to initialize Glean *on* the main thread, because it registers lifecycle // We have to initialize Glean *on* the main thread, because it registers lifecycle
// observers. However, the activation ping must be sent *off* of the main thread, // observers. However, the activation ping must be sent *off* of the main thread,
// because it calls Google ad APIs that must be called *off* of the main thread. // because it calls Google ad APIs that must be called *off* of the main thread.

@ -7,6 +7,7 @@ package org.mozilla.fenix.components.metrics
import android.app.Application import android.app.Application
import android.content.Context.MODE_PRIVATE import android.content.Context.MODE_PRIVATE
import android.net.Uri import android.net.Uri
import android.os.StrictMode
import android.util.Log import android.util.Log
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.leanplum.Leanplum import com.leanplum.Leanplum
@ -22,6 +23,7 @@ import kotlinx.coroutines.withContext
import mozilla.components.support.locale.LocaleManager import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.components.metrics.MozillaProductDetector.MozillaProducts import org.mozilla.fenix.components.metrics.MozillaProductDetector.MozillaProducts
import org.mozilla.fenix.ext.resetPoliciesAfter
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor
import java.util.Locale import java.util.Locale
@ -81,7 +83,9 @@ class LeanplumMetricsService(
override val type = MetricServiceType.Marketing override val type = MetricServiceType.Marketing
private val token = Token(LeanplumId, LeanplumToken) private val token = Token(LeanplumId, LeanplumToken)
private val preferences = application.getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE) private val preferences = StrictMode.allowThreadDiskReads().resetPoliciesAfter {
application.getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE)
}
@VisibleForTesting @VisibleForTesting
internal val deviceId by lazy { internal val deviceId by lazy {

@ -5,6 +5,7 @@
package org.mozilla.fenix.components.metrics package org.mozilla.fenix.components.metrics
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.leanplum.Leanplum
import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts
import mozilla.components.browser.menu.facts.BrowserMenuFacts import mozilla.components.browser.menu.facts.BrowserMenuFacts
import mozilla.components.browser.toolbar.facts.ToolbarFacts import mozilla.components.browser.toolbar.facts.ToolbarFacts
@ -193,6 +194,7 @@ internal class ReleaseMetricController(
if (installedAddons is List<*>) { if (installedAddons is List<*>) {
Addons.installedAddons.set(installedAddons.map { it.toString() }) Addons.installedAddons.set(installedAddons.map { it.toString() })
Addons.hasInstalledAddons.set(installedAddons.size > 0) Addons.hasInstalledAddons.set(installedAddons.size > 0)
Leanplum.setUserAttributes(mapOf("installed_addons" to installedAddons.size))
} }
} }
@ -200,6 +202,7 @@ internal class ReleaseMetricController(
if (enabledAddons is List<*>) { if (enabledAddons is List<*>) {
Addons.enabledAddons.set(enabledAddons.map { it.toString() }) Addons.enabledAddons.set(enabledAddons.map { it.toString() })
Addons.hasEnabledAddons.set(enabledAddons.size > 0) Addons.hasEnabledAddons.set(enabledAddons.size > 0)
Leanplum.setUserAttributes(mapOf("enabled_addons" to enabledAddons.size))
} }
} }

@ -23,6 +23,7 @@ import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.session.SessionFeature
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -34,7 +35,6 @@ import org.mozilla.fenix.browser.readermode.ReaderModeController
import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.TopSiteStorage
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.getRootView
@ -75,13 +75,12 @@ class DefaultBrowserToolbarController(
private val bookmarkTapped: (Session) -> Unit, private val bookmarkTapped: (Session) -> Unit,
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val tabCollectionStorage: TabCollectionStorage, private val tabCollectionStorage: TabCollectionStorage,
private val topSiteStorage: TopSiteStorage,
private val onTabCounterClicked: () -> Unit, private val onTabCounterClicked: () -> Unit,
private val onCloseTab: (Session) -> Unit private val onCloseTab: (Session) -> Unit
) : BrowserToolbarController { ) : BrowserToolbarController {
private val useNewSearchExperience private val useNewSearchExperience
get() = activity.settings().useNewSearchExperience get() = FeatureFlags.newSearchExperience
private val currentSession private val currentSession
get() = customTabSession ?: activity.components.core.sessionManager.selectedSession get() = customTabSession ?: activity.components.core.sessionManager.selectedSession
@ -245,7 +244,9 @@ class DefaultBrowserToolbarController(
scope.launch { scope.launch {
ioScope.launch { ioScope.launch {
currentSession?.let { currentSession?.let {
topSiteStorage.addTopSite(it.title, it.url) with(activity.components.useCases.topSitesUseCase) {
addPinnedSites(it.title, it.url)
}
} }
}.join() }.join()
@ -380,6 +381,13 @@ class DefaultBrowserToolbarController(
BrowserFragmentDirections.actionGlobalHistoryFragment() BrowserFragmentDirections.actionGlobalHistoryFragment()
) )
} }
ToolbarMenu.Item.Downloads -> browserAnimator.captureEngineViewAndDrawStatically {
navController.nav(
R.id.browserFragment,
BrowserFragmentDirections.actionGlobalDownloadsFragment()
)
}
} }
} }
@ -414,6 +422,7 @@ class DefaultBrowserToolbarController(
ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER
ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS
ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY
ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS
} }
activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem)) activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem))

@ -177,11 +177,14 @@ class DefaultToolbarMenu(
?.browsingModeManager?.mode == BrowsingMode.Normal ?.browsingModeManager?.mode == BrowsingMode.Normal
val shouldDeleteDataOnQuit = context.components.settings val shouldDeleteDataOnQuit = context.components.settings
.shouldDeleteBrowsingDataOnQuit .shouldDeleteBrowsingDataOnQuit
val syncedTabsInTabsTray = context.components.settings
.syncedTabsInTabsTray
val menuItems = listOfNotNull( val menuItems = listOfNotNull(
if (FeatureFlags.viewDownloads) downloadsItem else null,
historyItem, historyItem,
bookmarksItem, bookmarksItem,
if (FeatureFlags.syncedTabs) syncedTabs else null, if (syncedTabsInTabsTray) null else syncedTabs,
settings, settings,
if (shouldDeleteDataOnQuit) deleteDataOnQuit else null, if (shouldDeleteDataOnQuit) deleteDataOnQuit else null,
BrowserMenuDivider(), BrowserMenuDivider(),
@ -333,6 +336,14 @@ class DefaultToolbarMenu(
onItemTapped.invoke(ToolbarMenu.Item.Bookmarks) onItemTapped.invoke(ToolbarMenu.Item.Bookmarks)
} }
val downloadsItem = BrowserMenuImageText(
"Downloads",
R.drawable.ic_download,
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Downloads)
}
@ColorRes @ColorRes
private fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context) private fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context)

@ -12,6 +12,7 @@ import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.view.updatePadding
import kotlinx.android.synthetic.main.mozac_ui_tabcounter_layout.view.* import kotlinx.android.synthetic.main.mozac_ui_tabcounter_layout.view.*
import org.mozilla.fenix.R import org.mozilla.fenix.R
import java.text.NumberFormat import java.text.NumberFormat
@ -178,7 +179,7 @@ class TabCounter @JvmOverloads constructor(
private fun formatForDisplay(count: Int): String { private fun formatForDisplay(count: Int): String {
return if (count > MAX_VISIBLE_TABS) { return if (count > MAX_VISIBLE_TABS) {
counter_text.setPadding(0, 0, 0, INFINITE_CHAR_PADDING_BOTTOM) counter_text.updatePadding(bottom = INFINITE_CHAR_PADDING_BOTTOM)
SO_MANY_TABS_OPEN SO_MANY_TABS_OPEN
} else NumberFormat.getInstance().format(count.toLong()) } else NumberFormat.getInstance().format(count.toLong())
} }

@ -30,6 +30,7 @@ interface ToolbarMenu {
object ReaderModeAppearance : Item() object ReaderModeAppearance : Item()
object Bookmarks : Item() object Bookmarks : Item()
object History : Item() object History : Item()
object Downloads : Item()
} }
val menuBuilder: BrowserMenuBuilder val menuBuilder: BrowserMenuBuilder

@ -4,19 +4,15 @@
package org.mozilla.fenix.customtabs package org.mozilla.fenix.customtabs
import android.content.Intent
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import mozilla.components.browser.session.runWithSession import mozilla.components.browser.session.runWithSession
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.manifest.WebAppManifestParser import mozilla.components.concept.engine.manifest.WebAppManifestParser
import mozilla.components.feature.intent.ext.getSessionId import mozilla.components.feature.intent.ext.getSessionId
import mozilla.components.feature.pwa.ext.getWebAppManifest import mozilla.components.feature.pwa.ext.getWebAppManifest
import mozilla.components.feature.search.SearchAdapter
import mozilla.components.support.utils.SafeIntent import mozilla.components.support.utils.SafeIntent
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
@ -28,12 +24,6 @@ import java.security.InvalidParameterException
*/ */
open class ExternalAppBrowserActivity : HomeActivity() { open class ExternalAppBrowserActivity : HomeActivity() {
private val openInFenixIntent by lazy {
Intent(this, IntentReceiverActivity::class.java).apply {
action = Intent.ACTION_VIEW
}
}
final override fun getBreadcrumbMessage(destination: NavDestination): String { final override fun getBreadcrumbMessage(destination: NavDestination): String {
val fragmentName = resources.getResourceEntryName(destination.id) val fragmentName = resources.getResourceEntryName(destination.id)
return "Changing to fragment $fragmentName, isCustomTab: true" return "Changing to fragment $fragmentName, isCustomTab: true"
@ -45,8 +35,8 @@ open class ExternalAppBrowserActivity : HomeActivity() {
final override fun getIntentSessionId(intent: SafeIntent) = intent.getSessionId() final override fun getIntentSessionId(intent: SafeIntent) = intent.getSessionId()
override fun setAppAllStartTelemetry(safeIntent: SafeIntent) { override fun startupTelemetryOnCreateCalled(safeIntent: SafeIntent, hasSavedInstanceState: Boolean) {
components.appAllSourceStartTelemetry.receivedIntentInExternalAppBrowserActivity(safeIntent) components.appStartupTelemetry.onExternalAppBrowserOnCreate(safeIntent, hasSavedInstanceState)
} }
override fun getNavDirections( override fun getNavDirections(
@ -73,19 +63,6 @@ open class ExternalAppBrowserActivity : HomeActivity() {
} }
} }
override fun getSearchAdapter(store: BrowserStore): SearchAdapter {
val baseAdapter = super.getSearchAdapter(store)
return object : SearchAdapter {
override fun sendSearch(isPrivate: Boolean, text: String) {
baseAdapter.sendSearch(isPrivate, text)
startActivity(openInFenixIntent)
}
override fun isPrivateSession() = baseAdapter.isPrivateSession()
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()

@ -4,9 +4,9 @@
package org.mozilla.fenix.customtabs package org.mozilla.fenix.customtabs
import android.app.Notification
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.app.NotificationCompat
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.manifest.WebAppManifest import mozilla.components.concept.engine.manifest.WebAppManifest
@ -23,12 +23,8 @@ class WebAppSiteControlsBuilder(
private val inner = SiteControlsBuilder.CopyAndRefresh(reloadUrlUseCase) private val inner = SiteControlsBuilder.CopyAndRefresh(reloadUrlUseCase)
override fun buildNotification( override fun buildNotification(context: Context, builder: Notification.Builder) {
context: Context, inner.buildNotification(context, builder)
builder: NotificationCompat.Builder,
channelId: String
) {
inner.buildNotification(context, builder, channelId)
val isPrivateSession = sessionManager.findSessionById(sessionId)?.private ?: false val isPrivateSession = sessionManager.findSessionById(sessionId)?.private ?: false

@ -11,6 +11,7 @@ import androidx.annotation.StringRes
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import mozilla.components.ui.widgets.WidgetSiteItemView
import org.mozilla.fenix.exceptions.viewholders.ExceptionsDeleteButtonViewHolder import org.mozilla.fenix.exceptions.viewholders.ExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsHeaderViewHolder import org.mozilla.fenix.exceptions.viewholders.ExceptionsHeaderViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsListItemViewHolder import org.mozilla.fenix.exceptions.viewholders.ExceptionsListItemViewHolder
@ -67,7 +68,7 @@ abstract class ExceptionsAdapter<T : Any>(
ExceptionsHeaderViewHolder.LAYOUT_ID -> ExceptionsHeaderViewHolder.LAYOUT_ID ->
ExceptionsHeaderViewHolder(view, headerDescriptionResource) ExceptionsHeaderViewHolder(view, headerDescriptionResource)
ExceptionsListItemViewHolder.LAYOUT_ID -> ExceptionsListItemViewHolder.LAYOUT_ID ->
ExceptionsListItemViewHolder(view, interactor) ExceptionsListItemViewHolder(view as WidgetSiteItemView, interactor)
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }

@ -4,39 +4,41 @@
package org.mozilla.fenix.exceptions.viewholders package org.mozilla.fenix.exceptions.viewholders
import android.view.View import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.exception_item.*
import mozilla.components.browser.icons.BrowserIcons import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.ui.widgets.WidgetSiteItemView
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.exceptions.ExceptionsInteractor import org.mozilla.fenix.exceptions.ExceptionsInteractor
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.utils.view.ViewHolder
/** /**
* View holder for a single website that is exempted from Tracking Protection or Logins. * View holder for a single website that is exempted from Tracking Protection or Logins.
*/ */
class ExceptionsListItemViewHolder<T : Any>( class ExceptionsListItemViewHolder<T : Any>(
view: View, private val view: WidgetSiteItemView,
private val interactor: ExceptionsInteractor<T>, private val interactor: ExceptionsInteractor<T>,
private val icons: BrowserIcons = view.context.components.core.icons private val icons: BrowserIcons = view.context.components.core.icons
) : ViewHolder(view) { ) : RecyclerView.ViewHolder(view) {
private lateinit var item: T private lateinit var item: T
init { init {
delete_exception.setOnClickListener { view.setSecondaryButton(
icon = R.drawable.ic_close,
contentDescription = R.string.history_delete_item
) {
interactor.onDeleteOne(item) interactor.onDeleteOne(item)
} }
} }
fun bind(item: T, url: String) { fun bind(item: T, url: String) {
this.item = item this.item = item
webAddressView.text = url view.setText(label = url, caption = null)
icons.loadIntoView(favicon_image, url) icons.loadIntoView(view.iconView, url)
} }
companion object { companion object {
const val LAYOUT_ID = R.layout.exception_item const val LAYOUT_ID = R.layout.site_list_item
} }
} }

@ -7,6 +7,7 @@ package org.mozilla.fenix.ext
import android.app.Activity import android.app.Activity
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import mozilla.components.support.base.crash.Breadcrumb
/** /**
* Attempts to call immersive mode using the View to hide the status bar and navigation buttons. * Attempts to call immersive mode using the View to hide the status bar and navigation buttons.
@ -22,3 +23,19 @@ fun Activity.enterToImmersiveMode() {
or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
} }
fun Activity.breadcrumb(
message: String,
data: Map<String, String> = emptyMap()
) {
components.analytics.crashReporter.recordCrashBreadcrumb(
Breadcrumb(
category = this::class.java.simpleName,
message = message,
data = data + mapOf(
"instance" to this.hashCode().toString()
),
level = Breadcrumb.Level.INFO
)
)
}

@ -6,6 +6,9 @@ package org.mozilla.fenix.ext
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.view.ContextThemeWrapper import android.view.ContextThemeWrapper
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -89,3 +92,21 @@ fun Context.getStringWithArgSafe(@StringRes resId: Int, formatArg: String): Stri
*/ */
val Context.accessibilityManager: AccessibilityManager get() = val Context.accessibilityManager: AccessibilityManager get() =
getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
/**
* Used to navigate to system notifications settings for app
*/
fun Context.navigateToNotificationsSettings() {
val intent = Intent()
intent.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
it.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
it.putExtra(Settings.EXTRA_APP_PACKAGE, this.packageName)
} else {
it.action = "android.settings.APP_NOTIFICATION_SETTINGS"
it.putExtra("app_package", this.packageName)
it.putExtra("app_uid", this.applicationInfo.uid)
}
}
startActivity(intent)
}

@ -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.ext
import org.mozilla.fenix.R
import org.mozilla.fenix.library.downloads.DownloadItem
// While this looks complex, it's actually pretty simple.
@SuppressWarnings("ComplexMethod")
fun DownloadItem.getIcon(): Int {
fun getIconCornerCases(fileName: String?): Int {
return when {
fileName?.endsWith("apk") == true -> R.drawable.ic_file_type_apk
fileName?.endsWith("zip") == true -> R.drawable.ic_file_type_zip
else -> R.drawable.ic_file_type_default
}
}
fun checkForApplicationArchiveSubtypes(contentType: String): Int? {
return when {
contentType.contains("rar") -> R.drawable.ic_file_type_zip
contentType.contains("zip") -> R.drawable.ic_file_type_zip
contentType.contains("7z") -> R.drawable.ic_file_type_zip
contentType.contains("tar") -> R.drawable.ic_file_type_zip
contentType.contains("freearc") -> R.drawable.ic_file_type_zip
contentType.contains("octet-stream") -> null
contentType.contains("vnd.android.package-archive") -> null
else -> R.drawable.ic_file_type_document
}
}
fun getIconFromContentType(contentType: String): Int? {
return when {
contentType.contains("image/") -> R.drawable.ic_file_type_image
contentType.contains("audio/") -> R.drawable.ic_file_type_audio_note
contentType.contains("video/") -> R.drawable.ic_file_type_video
contentType.contains("application/") -> checkForApplicationArchiveSubtypes(contentType)
contentType.contains("text/") -> R.drawable.ic_file_type_document
else -> null
}
}
return contentType?.let { contentType ->
getIconFromContentType(contentType)
} ?: getIconCornerCases(fileName)
}

@ -0,0 +1,14 @@
/* 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 android.widget.EditText
/**
* Places cursor at the end of an EditText.
*/
fun EditText.placeCursorAtEnd() {
this.text?.length?.let { setSelection(it, it) }
}

@ -12,6 +12,7 @@ import androidx.navigation.NavDirections
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import androidx.navigation.fragment.NavHostFragment.findNavController import androidx.navigation.fragment.NavHostFragment.findNavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import mozilla.components.support.base.crash.Breadcrumb
import org.mozilla.fenix.NavHostActivity import org.mozilla.fenix.NavHostActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
@ -57,3 +58,23 @@ fun Fragment.redirectToReAuth(destinations: List<Int>, currentDestination: Int?)
findNavController().popBackStack(R.id.savedLoginsAuthFragment, false) findNavController().popBackStack(R.id.savedLoginsAuthFragment, false)
} }
} }
fun Fragment.breadcrumb(
message: String,
data: Map<String, String> = emptyMap()
) {
val activityName = activity?.let { it::class.java.simpleName } ?: "null"
requireComponents.analytics.crashReporter.recordCrashBreadcrumb(
Breadcrumb(
category = this::class.java.simpleName,
message = message,
data = data + mapOf(
"instance" to hashCode().toString(),
"activityInstance" to activity?.hashCode().toString(),
"activityName" to activityName
),
level = Breadcrumb.Level.INFO
)
)
}

@ -0,0 +1,20 @@
/* 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 org.mozilla.fenix.library.downloads.DownloadItem
import java.io.File
/**
* Checks a List of DownloadItems to verify whether items
* on that list are present on the disk or not. If a user has
* deleted the downloaded item it should not show on the downloaded
* list.
*/
fun List<DownloadItem>.filterNotExistsOnDisk(): List<DownloadItem> {
return this.filter {
File(it.filePath).exists()
}
}

@ -1,20 +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.ext
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
/**
* Observe a LiveData once and unregister from it as soon as the live data returns a value
*/
fun <T> LiveData<T>.observeOnce(observer: (T) -> Unit) {
observeForever(object : Observer<T> {
override fun onChanged(value: T) {
removeObserver(this)
observer(value)
}
})
}

@ -9,7 +9,7 @@ import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.style.AbsoluteSizeSpan import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import androidx.core.content.ContextCompat import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.support.ktx.android.util.dpToPx
fun SpannableString.setTextSize(context: Context, textSize: Int) = fun SpannableString.setTextSize(context: Context, textSize: Int) =
@ -23,10 +23,7 @@ fun SpannableString.setTextSize(context: Context, textSize: Int) =
fun SpannableString.setTextColor(context: Context, colorResId: Int) = fun SpannableString.setTextColor(context: Context, colorResId: Int) =
this.setSpan( this.setSpan(
ForegroundColorSpan( ForegroundColorSpan(
ContextCompat.getColor( context.getColorFromAttr(colorResId)
context,
colorResId
)
), ),
0, 0,
this.length, this.length,

@ -16,7 +16,6 @@ import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.widget.Button import android.widget.Button
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.PopupWindow import android.widget.PopupWindow
@ -68,10 +67,13 @@ import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSitesConfig
import mozilla.components.feature.top.sites.TopSitesFeature
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
@ -97,6 +99,7 @@ import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.home.sessioncontrol.SessionControlView import org.mozilla.fenix.home.sessioncontrol.SessionControlView
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.DefaultTopSitesView
import org.mozilla.fenix.onboarding.FenixOnboarding import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
@ -146,8 +149,11 @@ class HomeFragment : Fragment() {
private val store: BrowserStore private val store: BrowserStore
get() = requireComponents.core.store get() = requireComponents.core.store
private val onboarding by lazy { StrictMode.allowThreadDiskReads().resetPoliciesAfter { private val onboarding by lazy {
FenixOnboarding(requireContext()) } } StrictMode.allowThreadDiskReads().resetPoliciesAfter {
FenixOnboarding(requireContext())
}
}
private lateinit var homeFragmentStore: HomeFragmentStore private lateinit var homeFragmentStore: HomeFragmentStore
private var _sessionControlInteractor: SessionControlInteractor? = null private var _sessionControlInteractor: SessionControlInteractor? = null
@ -157,6 +163,8 @@ class HomeFragment : Fragment() {
private var sessionControlView: SessionControlView? = null private var sessionControlView: SessionControlView? = null
private lateinit var currentMode: CurrentMode private lateinit var currentMode: CurrentMode
private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
postponeEnterTransition() postponeEnterTransition()
@ -190,22 +198,31 @@ class HomeFragment : Fragment() {
collections = components.core.tabCollectionStorage.cachedTabCollections, collections = components.core.tabCollectionStorage.cachedTabCollections,
expandedCollections = emptySet(), expandedCollections = emptySet(),
mode = currentMode.getCurrentMode(), mode = currentMode.getCurrentMode(),
topSites = StrictMode.allowThreadDiskReads().resetPoliciesAfter { topSites = components.core.topSiteStorage.cachedTopSites,
components.core.topSiteStorage.cachedTopSites tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip(),
}, showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip()
) )
) )
} }
topSitesFeature.set(
feature = TopSitesFeature(
view = DefaultTopSitesView(homeFragmentStore),
storage = components.core.topSiteStorage,
config = ::getTopSitesConfig
),
owner = this,
view = view
)
_sessionControlInteractor = SessionControlInteractor( _sessionControlInteractor = SessionControlInteractor(
DefaultSessionControlController( DefaultSessionControlController(
activity = activity, activity = activity,
settings = components.settings,
engine = components.core.engine, engine = components.core.engine,
metrics = components.analytics.metrics, metrics = components.analytics.metrics,
sessionManager = sessionManager, sessionManager = sessionManager,
tabCollectionStorage = components.core.tabCollectionStorage, tabCollectionStorage = components.core.tabCollectionStorage,
topSiteStorage = components.core.topSiteStorage,
addTabUseCase = components.useCases.tabsUseCases.addTab, addTabUseCase = components.useCases.tabsUseCases.addTab,
fragmentStore = homeFragmentStore, fragmentStore = homeFragmentStore,
navController = findNavController(), navController = findNavController(),
@ -220,9 +237,9 @@ class HomeFragment : Fragment() {
updateLayout(view) updateLayout(view)
sessionControlView = SessionControlView( sessionControlView = SessionControlView(
view.sessionControlRecyclerView, view.sessionControlRecyclerView,
viewLifecycleOwner,
sessionControlInteractor, sessionControlInteractor,
homeViewModel, homeViewModel
requireComponents.core.store.state.normalTabs.isNotEmpty()
) )
updateSessionControlView(view) updateSessionControlView(view)
@ -231,6 +248,15 @@ class HomeFragment : Fragment() {
return view return view
} }
/**
* Returns a [TopSitesConfig] which specifies how many top sites to display and whether or
* not frequently visited sites should be displayed.
*/
private fun getTopSitesConfig(): TopSitesConfig {
val settings = requireContext().settings()
return TopSitesConfig(settings.topSitesMaxLimit, settings.showTopFrecentSites)
}
/** /**
* The [SessionControlView] is forced to update with our current state when we call * The [SessionControlView] is forced to update with our current state when we call
* [HomeFragment.onCreateView] in order to be able to draw everything at once with the current * [HomeFragment.onCreateView] in order to be able to draw everything at once with the current
@ -344,7 +370,7 @@ class HomeFragment : Fragment() {
view.toolbar_wrapper.setOnLongClickListener { view.toolbar_wrapper.setOnLongClickListener {
ToolbarPopupWindow.show( ToolbarPopupWindow.show(
WeakReference(view), WeakReference(it),
handlePasteAndGo = sessionControlInteractor::onPasteAndGo, handlePasteAndGo = sessionControlInteractor::onPasteAndGo,
handlePaste = sessionControlInteractor::onPaste, handlePaste = sessionControlInteractor::onPaste,
copyVisible = false copyVisible = false
@ -374,7 +400,8 @@ class HomeFragment : Fragment() {
// We call this onLayout so that the bottom bar width is correctly set for us to center // We call this onLayout so that the bottom bar width is correctly set for us to center
// the CFR in. // the CFR in.
view.toolbar_wrapper.doOnLayout { view.toolbar_wrapper.doOnLayout {
if (!browsingModeManager.mode.isPrivate) { val willNavigateToSearch = !bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience
if (!browsingModeManager.mode.isPrivate && !willNavigateToSearch) {
SearchWidgetCFR( SearchWidgetCFR(
context = view.context, context = view.context,
settings = view.context.settings(), settings = view.context.settings(),
@ -385,19 +412,6 @@ class HomeFragment : Fragment() {
} }
} }
val args by navArgs<HomeFragmentArgs>()
if (view.context.settings().accessibilityServicesEnabled &&
args.focusOnAddressBar
) {
// We cannot put this in the fragment_home.xml file as it breaks tests
view.toolbar_wrapper.isFocusableInTouchMode = true
viewLifecycleOwner.lifecycleScope.launch {
view.toolbar_wrapper?.requestFocus()
view.toolbar_wrapper?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
}
}
if (browsingModeManager.mode.isPrivate) { if (browsingModeManager.mode.isPrivate) {
requireActivity().window.addFlags(FLAG_SECURE) requireActivity().window.addFlags(FLAG_SECURE)
} else { } else {
@ -418,7 +432,7 @@ class HomeFragment : Fragment() {
updateTabCounter(requireComponents.core.store.state) updateTabCounter(requireComponents.core.store.state)
if (args.focusOnAddressBar && requireContext().settings().useNewSearchExperience) { if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience) {
navigateToSearch() navigateToSearch()
} }
} }
@ -506,7 +520,6 @@ class HomeFragment : Fragment() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
subscribeToTabCollections() subscribeToTabCollections()
subscribeToTopSites()
val context = requireContext() val context = requireContext()
val components = context.components val components = context.components
@ -516,7 +529,8 @@ class HomeFragment : Fragment() {
collections = components.core.tabCollectionStorage.cachedTabCollections, collections = components.core.tabCollectionStorage.cachedTabCollections,
mode = currentMode.getCurrentMode(), mode = currentMode.getCurrentMode(),
topSites = components.core.topSiteStorage.cachedTopSites, topSites = components.core.topSiteStorage.cachedTopSites,
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip() tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip(),
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
) )
) )
@ -556,6 +570,10 @@ class HomeFragment : Fragment() {
// We only want this observer live just before we navigate away to the collection creation screen // We only want this observer live just before we navigate away to the collection creation screen
requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver) requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver)
lifecycleScope.launch(IO) {
requireComponents.reviewPromptController.promptReview(requireActivity())
}
} }
private fun dispatchModeChanges(mode: Mode) { private fun dispatchModeChanges(mode: Mode) {
@ -679,7 +697,7 @@ class HomeFragment : Fragment() {
} }
private fun navigateToSearch() { private fun navigateToSearch() {
val directions = if (requireContext().settings().useNewSearchExperience) { val directions = if (FeatureFlags.newSearchExperience) {
HomeFragmentDirections.actionGlobalSearchDialog( HomeFragmentDirections.actionGlobalSearchDialog(
sessionId = null sessionId = null
) )
@ -771,6 +789,15 @@ class HomeFragment : Fragment() {
HomeFragmentDirections.actionGlobalHistoryFragment() HomeFragmentDirections.actionGlobalHistoryFragment()
) )
} }
HomeMenu.Item.Downloads -> {
hideOnboardingIfNeeded()
nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalDownloadsFragment()
)
}
HomeMenu.Item.Help -> { HomeMenu.Item.Help -> {
hideOnboardingIfNeeded() hideOnboardingIfNeeded()
(activity as HomeActivity).openToBrowserAndLoad( (activity as HomeActivity).openToBrowserAndLoad(
@ -832,17 +859,6 @@ class HomeFragment : Fragment() {
} }
} }
private fun subscribeToTopSites(): Observer<List<TopSite>> {
return Observer<List<TopSite>> { topSites ->
requireComponents.core.topSiteStorage.cachedTopSites = topSites
context?.settings()?.preferences?.edit()
?.putInt(getString(R.string.pref_key_top_sites_size), topSites.size)?.apply()
homeFragmentStore.dispatch(HomeFragmentAction.TopSitesChange(topSites))
}.also { observer ->
requireComponents.core.topSiteStorage.getTopSites().observe(this, observer)
}
}
private fun registerCollectionStorageObserver() { private fun registerCollectionStorageObserver() {
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this) requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
} }

@ -41,13 +41,16 @@ data class Tab(
* @property mode The state of the [HomeFragment] UI. * @property mode The state of the [HomeFragment] UI.
* @property tabs The list of opened [Tab] in the [HomeFragment]. * @property tabs The list of opened [Tab] in the [HomeFragment].
* @property topSites The list of [TopSite] in the [HomeFragment]. * @property topSites The list of [TopSite] in the [HomeFragment].
* @property tip The current [Tip] to show on the [HomeFragment].
* @property showCollectionPlaceholder If true, shows a placeholder when there are no collections.
*/ */
data class HomeFragmentState( data class HomeFragmentState(
val collections: List<TabCollection>, val collections: List<TabCollection>,
val expandedCollections: Set<Long>, val expandedCollections: Set<Long>,
val mode: Mode, val mode: Mode,
val topSites: List<TopSite>, val topSites: List<TopSite>,
val tip: Tip? = null val tip: Tip? = null,
val showCollectionPlaceholder: Boolean
) : State ) : State
sealed class HomeFragmentAction : Action { sealed class HomeFragmentAction : Action {
@ -55,7 +58,8 @@ sealed class HomeFragmentAction : Action {
val topSites: List<TopSite>, val topSites: List<TopSite>,
val mode: Mode, val mode: Mode,
val collections: List<TabCollection>, val collections: List<TabCollection>,
val tip: Tip? = null val tip: Tip? = null,
val showCollectionPlaceholder: Boolean
) : ) :
HomeFragmentAction() HomeFragmentAction()
@ -66,6 +70,7 @@ sealed class HomeFragmentAction : Action {
data class ModeChange(val mode: Mode) : HomeFragmentAction() data class ModeChange(val mode: Mode) : HomeFragmentAction()
data class TopSitesChange(val topSites: List<TopSite>) : HomeFragmentAction() data class TopSitesChange(val topSites: List<TopSite>) : HomeFragmentAction()
data class RemoveTip(val tip: Tip) : HomeFragmentAction() data class RemoveTip(val tip: Tip) : HomeFragmentAction()
object RemoveCollectionsPlaceholder : HomeFragmentAction()
} }
private fun homeFragmentStateReducer( private fun homeFragmentStateReducer(
@ -93,6 +98,11 @@ private fun homeFragmentStateReducer(
is HomeFragmentAction.CollectionsChange -> state.copy(collections = action.collections) is HomeFragmentAction.CollectionsChange -> state.copy(collections = action.collections)
is HomeFragmentAction.ModeChange -> state.copy(mode = action.mode) is HomeFragmentAction.ModeChange -> state.copy(mode = action.mode)
is HomeFragmentAction.TopSitesChange -> state.copy(topSites = action.topSites) is HomeFragmentAction.TopSitesChange -> state.copy(topSites = action.topSites)
is HomeFragmentAction.RemoveTip -> { state.copy(tip = null) } is HomeFragmentAction.RemoveTip -> {
state.copy(tip = null)
}
is HomeFragmentAction.RemoveCollectionsPlaceholder -> {
state.copy(showCollectionPlaceholder = false)
}
} }
} }

@ -43,6 +43,7 @@ class HomeMenu(
object SyncedTabs : Item() object SyncedTabs : Item()
object History : Item() object History : Item()
object Bookmarks : Item() object Bookmarks : Item()
object Downloads : Item()
object Quit : Item() object Quit : Item()
object Sync : Item() object Sync : Item()
} }
@ -144,6 +145,14 @@ class HomeMenu(
onItemTapped.invoke(Item.Help) onItemTapped.invoke(Item.Help)
} }
val downloadsItem = BrowserMenuImageText(
"Downloads",
R.drawable.ic_download,
primaryTextColor
) {
onItemTapped.invoke(Item.Downloads)
}
// Only query account manager if it has been initialized. // Only query account manager if it has been initialized.
// We don't want to cause its initialization just for this check. // We don't want to cause its initialization just for this check.
val accountAuthItem = if (context.components.backgroundServices.accountManagerAvailableQueue.isReady()) { val accountAuthItem = if (context.components.backgroundServices.accountManagerAvailableQueue.isReady()) {
@ -158,9 +167,10 @@ class HomeMenu(
if (settings.shouldDeleteBrowsingDataOnQuit) quitItem else null, if (settings.shouldDeleteBrowsingDataOnQuit) quitItem else null,
settingsItem, settingsItem,
BrowserMenuDivider(), BrowserMenuDivider(),
if (FeatureFlags.syncedTabs) syncedTabsItem else null, if (settings.syncedTabsInTabsTray) null else syncedTabsItem,
bookmarksItem, bookmarksItem,
historyItem, historyItem,
if (FeatureFlags.viewDownloads) downloadsItem else null,
BrowserMenuDivider(), BrowserMenuDivider(),
addons, addons,
BrowserMenuDivider(), BrowserMenuDivider(),

@ -8,11 +8,13 @@ import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.home.OnboardingState import org.mozilla.fenix.home.OnboardingState
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
@ -20,7 +22,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSiteViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSitePagerViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingAutomaticSignInViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingAutomaticSignInViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingFinishViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingFinishViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder
@ -40,14 +42,14 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
ButtonTipViewHolder.LAYOUT_ID ButtonTipViewHolder.LAYOUT_ID
) )
data class TopSiteList(val topSites: List<TopSite>) : AdapterItem(TopSiteViewHolder.LAYOUT_ID) { data class TopSitePager(val topSites: List<TopSite>) : AdapterItem(TopSitePagerViewHolder.LAYOUT_ID) {
override fun sameAs(other: AdapterItem): Boolean { override fun sameAs(other: AdapterItem): Boolean {
val newTopSites = (other as? TopSiteList) ?: return false val newTopSites = (other as? TopSitePager) ?: return false
return newTopSites.topSites == this.topSites return newTopSites.topSites == this.topSites
} }
override fun contentsSameAs(other: AdapterItem): Boolean { override fun contentsSameAs(other: AdapterItem): Boolean {
val newTopSites = (other as? TopSiteList) ?: return false val newTopSites = (other as? TopSitePager) ?: return false
if (newTopSites.topSites.size != this.topSites.size) return false if (newTopSites.topSites.size != this.topSites.size) return false
val newSitesSequence = newTopSites.topSites.asSequence() val newSitesSequence = newTopSites.topSites.asSequence()
val oldTopSites = this.topSites.asSequence() val oldTopSites = this.topSites.asSequence()
@ -135,7 +137,8 @@ class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
class SessionControlAdapter( class SessionControlAdapter(
private val interactor: SessionControlInteractor, private val interactor: SessionControlInteractor,
private val hasNormalTabsOpened: Boolean private val viewLifecycleOwner: LifecycleOwner,
private val components: Components
) : ListAdapter<AdapterItem, RecyclerView.ViewHolder>(AdapterItemDiffCallback()) { ) : ListAdapter<AdapterItem, RecyclerView.ViewHolder>(AdapterItemDiffCallback()) {
// This method triggers the ComplexMethod lint error when in fact it's quite simple. // This method triggers the ComplexMethod lint error when in fact it's quite simple.
@ -144,13 +147,18 @@ class SessionControlAdapter(
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) { return when (viewType) {
ButtonTipViewHolder.LAYOUT_ID -> ButtonTipViewHolder(view, interactor) ButtonTipViewHolder.LAYOUT_ID -> ButtonTipViewHolder(view, interactor)
TopSiteViewHolder.LAYOUT_ID -> TopSiteViewHolder(view, interactor) TopSitePagerViewHolder.LAYOUT_ID -> TopSitePagerViewHolder(view, interactor)
PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder( PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(
view, view,
interactor interactor
) )
NoCollectionsMessageViewHolder.LAYOUT_ID -> NoCollectionsMessageViewHolder.LAYOUT_ID ->
NoCollectionsMessageViewHolder(view, interactor, hasNormalTabsOpened) NoCollectionsMessageViewHolder(
view,
viewLifecycleOwner,
components.core.store,
interactor
)
CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view) CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view)
CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, interactor) CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, interactor)
TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder( TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder(
@ -195,8 +203,8 @@ class SessionControlAdapter(
val tipItem = item as AdapterItem.TipItem val tipItem = item as AdapterItem.TipItem
holder.bind(tipItem.tip) holder.bind(tipItem.tip)
} }
is TopSiteViewHolder -> { is TopSitePagerViewHolder -> {
holder.bind((item as AdapterItem.TopSiteList).topSites) holder.bind((item as AdapterItem.TopSitePager).topSites)
} }
is CollectionViewHolder -> { is CollectionViewHolder -> {
val (collection, expanded) = item as AdapterItem.CollectionItem val (collection, expanded) = item as AdapterItem.CollectionItem

@ -22,7 +22,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.TopSiteStorage
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsUtils import org.mozilla.fenix.components.metrics.MetricsUtils
@ -37,6 +36,7 @@ import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.HomeFragmentStore import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings
import mozilla.components.feature.tab.collections.Tab as ComponentTab import mozilla.components.feature.tab.collections.Tab as ComponentTab
/** /**
@ -144,16 +144,21 @@ interface SessionControlController {
* @see [CollectionInteractor.onAddTabsToCollectionTapped] * @see [CollectionInteractor.onAddTabsToCollectionTapped]
*/ */
fun handleCreateCollection() fun handleCreateCollection()
/**
* @see [CollectionInteractor.onRemoveCollectionsPlaceholder]
*/
fun handleRemoveCollectionsPlaceholder()
} }
@Suppress("TooManyFunctions", "LargeClass") @Suppress("TooManyFunctions", "LargeClass")
class DefaultSessionControlController( class DefaultSessionControlController(
private val activity: HomeActivity, private val activity: HomeActivity,
private val settings: Settings,
private val engine: Engine, private val engine: Engine,
private val metrics: MetricController, private val metrics: MetricController,
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val tabCollectionStorage: TabCollectionStorage, private val tabCollectionStorage: TabCollectionStorage,
private val topSiteStorage: TopSiteStorage,
private val addTabUseCase: TabsUseCases.AddNewTabUseCase, private val addTabUseCase: TabsUseCases.AddNewTabUseCase,
private val fragmentStore: HomeFragmentStore, private val fragmentStore: HomeFragmentStore,
private val navController: NavController, private val navController: NavController,
@ -213,7 +218,11 @@ class DefaultSessionControlController(
metrics.track(Event.CollectionAllTabsRestored) metrics.track(Event.CollectionAllTabsRestored)
} }
override fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab, wasSwiped: Boolean) { override fun handleCollectionRemoveTab(
collection: TabCollection,
tab: ComponentTab,
wasSwiped: Boolean
) {
metrics.track(Event.CollectionTabRemoved) metrics.track(Event.CollectionTabRemoved)
if (collection.tabs.size == 1) { if (collection.tabs.size == 1) {
@ -223,7 +232,13 @@ class DefaultSessionControlController(
) )
val message = val message =
activity.resources.getString(R.string.delete_tab_and_collection_dialog_message) activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
showDeleteCollectionPrompt(collection, title, message, wasSwiped, handleSwipedItemDeletionCancel) showDeleteCollectionPrompt(
collection,
title,
message,
wasSwiped,
handleSwipedItemDeletionCancel
)
} else { } else {
viewLifecycleScope.launch(Dispatchers.IO) { viewLifecycleScope.launch(Dispatchers.IO) {
tabCollectionStorage.removeTabFromCollection(collection, tab) tabCollectionStorage.removeTabFromCollection(collection, tab)
@ -273,7 +288,9 @@ class DefaultSessionControlController(
} }
viewLifecycleScope.launch(Dispatchers.IO) { viewLifecycleScope.launch(Dispatchers.IO) {
topSiteStorage.removeTopSite(topSite) with(activity.components.useCases.topSitesUseCase) {
removeTopSites(topSite)
}
} }
} }
@ -369,6 +386,11 @@ class DefaultSessionControlController(
showTabTrayCollectionCreation() showTabTrayCollectionCreation()
} }
override fun handleRemoveCollectionsPlaceholder() {
settings.showCollectionsPlaceholderOnHome = false
fragmentStore.dispatch(HomeFragmentAction.RemoveCollectionsPlaceholder)
}
private fun showShareFragment(shareSubject: String, data: List<ShareData>) { private fun showShareFragment(shareSubject: String, data: List<ShareData>) {
val directions = HomeFragmentDirections.actionGlobalShareFragment( val directions = HomeFragmentDirections.actionGlobalShareFragment(
shareSubject = shareSubject, shareSubject = shareSubject,

@ -93,6 +93,11 @@ interface CollectionInteractor {
* Opens the collection creator * Opens the collection creator
*/ */
fun onAddTabsToCollectionTapped() fun onAddTabsToCollectionTapped()
/**
* User has removed the collections placeholder from home.
*/
fun onRemoveCollectionsPlaceholder()
} }
interface ToolbarInteractor { interface ToolbarInteractor {
@ -256,4 +261,8 @@ class SessionControlInteractor(
override fun onPaste(clipboardText: String) { override fun onPaste(clipboardText: String) {
controller.handlePaste(clipboardText) controller.handlePaste(clipboardText)
} }
override fun onRemoveCollectionsPlaceholder() {
controller.handleRemoveCollectionsPlaceholder()
}
} }

@ -5,6 +5,7 @@
package org.mozilla.fenix.home.sessioncontrol package org.mozilla.fenix.home.sessioncontrol
import android.view.View import android.view.View
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -13,6 +14,7 @@ import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.HomeFragmentState import org.mozilla.fenix.home.HomeFragmentState
import org.mozilla.fenix.home.HomeScreenViewModel import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.home.Mode import org.mozilla.fenix.home.Mode
@ -25,19 +27,21 @@ private fun normalModeAdapterItems(
topSites: List<TopSite>, topSites: List<TopSite>,
collections: List<TabCollection>, collections: List<TabCollection>,
expandedCollections: Set<Long>, expandedCollections: Set<Long>,
tip: Tip? tip: Tip?,
showCollectionsPlaceholder: Boolean
): List<AdapterItem> { ): List<AdapterItem> {
val items = mutableListOf<AdapterItem>() val items = mutableListOf<AdapterItem>()
tip?.let { items.add(AdapterItem.TipItem(it)) } tip?.let { items.add(AdapterItem.TipItem(it)) }
if (topSites.isNotEmpty()) { if (topSites.isNotEmpty()) {
items.add(AdapterItem.TopSiteList(topSites)) items.add(AdapterItem.TopSitePager(topSites))
} }
if (collections.isEmpty()) { if (collections.isEmpty()) {
items.add(AdapterItem.CollectionHeader) if (showCollectionsPlaceholder) {
items.add(AdapterItem.NoCollectionsMessage) items.add(AdapterItem.NoCollectionsMessage)
}
} else { } else {
showCollections(collections, expandedCollections, items) showCollections(collections, expandedCollections, items)
} }
@ -68,62 +72,77 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List<Adapt
val items: MutableList<AdapterItem> = mutableListOf(AdapterItem.OnboardingHeader) val items: MutableList<AdapterItem> = mutableListOf(AdapterItem.OnboardingHeader)
// Customize FxA items based on where we are with the account state: // Customize FxA items based on where we are with the account state:
items.addAll(when (onboardingState) { items.addAll(
OnboardingState.SignedOutNoAutoSignIn -> { when (onboardingState) {
listOf( OnboardingState.SignedOutNoAutoSignIn -> {
AdapterItem.OnboardingManualSignIn listOf(
) AdapterItem.OnboardingManualSignIn
} )
is OnboardingState.SignedOutCanAutoSignIn -> { }
listOf( is OnboardingState.SignedOutCanAutoSignIn -> {
AdapterItem.OnboardingAutomaticSignIn(onboardingState) listOf(
) AdapterItem.OnboardingAutomaticSignIn(onboardingState)
)
}
OnboardingState.SignedIn -> listOf()
} }
OnboardingState.SignedIn -> listOf() )
})
items.addAll(
items.addAll(listOf( listOf(
AdapterItem.OnboardingSectionHeader { AdapterItem.OnboardingSectionHeader {
val appName = it.getString(R.string.app_name) val appName = it.getString(R.string.app_name)
it.getString(R.string.onboarding_feature_section_header, appName) it.getString(R.string.onboarding_feature_section_header, appName)
}, },
AdapterItem.OnboardingWhatsNew, AdapterItem.OnboardingWhatsNew,
AdapterItem.OnboardingTrackingProtection, AdapterItem.OnboardingTrackingProtection,
AdapterItem.OnboardingThemePicker, AdapterItem.OnboardingThemePicker,
AdapterItem.OnboardingPrivateBrowsing, AdapterItem.OnboardingPrivateBrowsing,
AdapterItem.OnboardingToolbarPositionPicker, AdapterItem.OnboardingToolbarPositionPicker,
AdapterItem.OnboardingPrivacyNotice, AdapterItem.OnboardingPrivacyNotice,
AdapterItem.OnboardingFinish AdapterItem.OnboardingFinish
)) )
)
return items return items
} }
private fun HomeFragmentState.toAdapterList(): List<AdapterItem> = when (mode) { private fun HomeFragmentState.toAdapterList(): List<AdapterItem> = when (mode) {
is Mode.Normal -> normalModeAdapterItems(topSites, collections, expandedCollections, tip) is Mode.Normal -> normalModeAdapterItems(
topSites,
collections,
expandedCollections,
tip,
showCollectionPlaceholder
)
is Mode.Private -> privateModeAdapterItems() is Mode.Private -> privateModeAdapterItems()
is Mode.Onboarding -> onboardingAdapterItems(mode.state) is Mode.Onboarding -> onboardingAdapterItems(mode.state)
} }
private fun collectionTabItems(collection: TabCollection) = collection.tabs.mapIndexed { index, tab -> private fun collectionTabItems(collection: TabCollection) =
collection.tabs.mapIndexed { index, tab ->
AdapterItem.TabInCollectionItem(collection, tab, index == collection.tabs.lastIndex) AdapterItem.TabInCollectionItem(collection, tab, index == collection.tabs.lastIndex)
} }
class SessionControlView( class SessionControlView(
override val containerView: View?, override val containerView: View,
viewLifecycleOwner: LifecycleOwner,
interactor: SessionControlInteractor, interactor: SessionControlInteractor,
private var homeScreenViewModel: HomeScreenViewModel, private var homeScreenViewModel: HomeScreenViewModel
private val hasNormalTabsOpened: Boolean
) : LayoutContainer { ) : LayoutContainer {
val view: RecyclerView = containerView as RecyclerView val view: RecyclerView = containerView as RecyclerView
private val sessionControlAdapter = SessionControlAdapter(interactor, hasNormalTabsOpened) private val sessionControlAdapter = SessionControlAdapter(
interactor,
viewLifecycleOwner,
containerView.context.components
)
init { init {
view.apply { view.apply {
adapter = sessionControlAdapter adapter = sessionControlAdapter
layoutManager = LinearLayoutManager(containerView!!.context) layoutManager = LinearLayoutManager(containerView.context)
val itemTouchHelper = val itemTouchHelper =
ItemTouchHelper( ItemTouchHelper(
SwipeToDeleteCallback( SwipeToDeleteCallback(
@ -141,7 +160,7 @@ class SessionControlView(
sessionControlAdapter.submitList(stateAdapterList) { sessionControlAdapter.submitList(stateAdapterList) {
val loadedTopSites = stateAdapterList.find { adapterItem -> val loadedTopSites = stateAdapterList.find { adapterItem ->
adapterItem is AdapterItem.TopSiteList && adapterItem.topSites.isNotEmpty() adapterItem is AdapterItem.TopSitePager && adapterItem.topSites.isNotEmpty()
} }
loadedTopSites?.run { loadedTopSites?.run {
homeScreenViewModel.shouldScrollToTopSites = false homeScreenViewModel.shouldScrollToTopSites = false

@ -6,22 +6,50 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import kotlinx.android.synthetic.main.no_collections_message.* import kotlinx.android.synthetic.main.no_collections_message.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.utils.view.ViewHolder import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor
import org.mozilla.fenix.utils.view.ViewHolder
@OptIn(ExperimentalCoroutinesApi::class)
open class NoCollectionsMessageViewHolder( open class NoCollectionsMessageViewHolder(
view: View, view: View,
interactor: CollectionInteractor, viewLifecycleOwner: LifecycleOwner,
hasNormalTabsOpened: Boolean store: BrowserStore,
interactor: CollectionInteractor
) : ViewHolder(view) { ) : ViewHolder(view) {
init { init {
add_tabs_to_collections_button.setOnClickListener { add_tabs_to_collections_button.setOnClickListener {
interactor.onAddTabsToCollectionTapped() interactor.onAddTabsToCollectionTapped()
} }
add_tabs_to_collections_button.isVisible = hasNormalTabsOpened
remove_collection_placeholder.increaseTapArea(
view.resources.getDimensionPixelSize(R.dimen.tap_increase_16)
)
remove_collection_placeholder.setOnClickListener {
interactor.onRemoveCollectionsPlaceholder()
}
add_tabs_to_collections_button.isVisible = store.state.normalTabs.isNotEmpty()
store.flowScoped(viewLifecycleOwner) { flow ->
flow.map { state -> state.normalTabs.size }
.ifChanged()
.collect { tabs ->
add_tabs_to_collections_button.isVisible = tabs > 0
}
}
} }
companion object { companion object {

@ -0,0 +1,57 @@
/* 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.home.sessioncontrol.viewholders
import android.view.View
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import kotlinx.android.synthetic.main.component_top_sites_pager.view.*
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.TopSitesPagerAdapter
class TopSitePagerViewHolder(
view: View,
interactor: TopSiteInteractor
) : RecyclerView.ViewHolder(view) {
private val topSitesPagerAdapter = TopSitesPagerAdapter(interactor)
private val pageIndicator = view.page_indicator
private val topSitesPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
pageIndicator.setSelection(position)
}
}
init {
view.top_sites_pager.apply {
adapter = topSitesPagerAdapter
registerOnPageChangeCallback(topSitesPageChangeCallback)
}
}
fun bind(topSites: List<TopSite>) {
topSitesPagerAdapter.updateData(topSites)
// Don't show any page indicator if there is only 1 page.
val numPages = if (topSites.size > TOP_SITES_PER_PAGE) {
TOP_SITES_MAX_PAGE_SIZE
} else {
0
}
pageIndicator.isVisible = numPages > 1
pageIndicator.setSize(numPages)
}
companion object {
const val LAYOUT_ID = R.layout.component_top_sites_pager
const val TOP_SITES_MAX_PAGE_SIZE = 2
const val TOP_SITES_PER_PAGE = 8
}
}

@ -6,7 +6,6 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.FlexboxLayoutManager
import kotlinx.android.synthetic.main.component_top_sites.view.* import kotlinx.android.synthetic.main.component_top_sites.view.*
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -23,8 +22,6 @@ class TopSiteViewHolder(
init { init {
view.top_sites_list.apply { view.top_sites_list.apply {
adapter = topSitesAdapter adapter = topSitesAdapter
layoutManager = FlexboxLayoutManager(view.context)
isNestedScrollingEnabled = false
} }
} }

@ -31,9 +31,9 @@ class OnboardingAutomaticSignInViewHolder(
private val headerText = view.header_text private val headerText = view.header_text
init { init {
view.turn_on_sync_button.setOnClickListener { view.fxa_sign_in_button.setOnClickListener {
scope.launch { scope.launch {
onClick(it.turn_on_sync_button) onClick(it.fxa_sign_in_button)
} }
} }
} }

@ -5,40 +5,40 @@
package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.navigation.Navigation import androidx.navigation.Navigation
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.onboarding_manual_signin.view.* import kotlinx.android.synthetic.main.onboarding_manual_signin.view.*
import mozilla.components.support.ktx.android.content.getDrawableWithTint
import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.addUnderline
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.onboarding.OnboardingController
import org.mozilla.fenix.onboarding.OnboardingInteractor
class OnboardingManualSignInViewHolder(view: View) : RecyclerView.ViewHolder(view) { class OnboardingManualSignInViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val headerText = view.header_text private val headerText = view.header_text
init { init {
view.turn_on_sync_button.setOnClickListener { val interactor = OnboardingInteractor(OnboardingController(itemView.context))
view.fxa_sign_in_button.setOnClickListener {
it.context.components.analytics.metrics.track(Event.OnboardingManualSignIn) it.context.components.analytics.metrics.track(Event.OnboardingManualSignIn)
val directions = HomeFragmentDirections.actionGlobalTurnOnSync() val directions = HomeFragmentDirections.actionGlobalTurnOnSync()
Navigation.findNavController(view).navigate(directions) Navigation.findNavController(view).navigate(directions)
} }
view.learn_more.addUnderline()
view.learn_more.setOnClickListener {
interactor.onLearnMoreClicked()
}
} }
fun bind() { fun bind() {
val context = itemView.context val context = itemView.context
headerText.text = context.getString(R.string.onboarding_account_sign_in_header)
val appName = context.getString(R.string.app_name)
headerText.text = context.getString(R.string.onboarding_firefox_account_header, appName)
val icon = context.getDrawableWithTint(
R.drawable.ic_onboarding_firefox_accounts,
ContextCompat.getColor(context, R.color.white_color)
)
headerText.putCompoundDrawablesRelativeWithIntrinsicBounds(start = icon)
} }
companion object { companion object {

@ -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.home.sessioncontrol.viewholders.topsites
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.feature.top.sites.view.TopSitesView
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentStore
class DefaultTopSitesView(
val store: HomeFragmentStore
) : TopSitesView {
override fun displayTopSites(topSites: List<TopSite>) {
store.dispatch(HomeFragmentAction.TopSitesChange(topSites))
}
}

@ -0,0 +1,82 @@
/* 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.home.sessioncontrol.viewholders.topsites
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.widget.LinearLayout
import androidx.core.view.MarginLayoutParamsCompat
import org.mozilla.fenix.R
/**
* A pager indicator widget to display the number of pages and the current selected page.
*/
class PagerIndicator : LinearLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private var selectedIndex = 0
/**
* Set the number of pager dots to display.
*/
fun setSize(size: Int) {
if (childCount == size) {
return
}
if (selectedIndex >= size) {
selectedIndex = size - 1
}
removeAllViews()
for (i in 0 until size) {
val isLast = i == size - 1
addView(
View(context).apply {
setBackgroundResource(R.drawable.pager_dot)
isSelected = i == selectedIndex
},
LayoutParams(dpToPx(DOT_SIZE_IN_DP), dpToPx(DOT_SIZE_IN_DP)).apply {
if (!isLast) {
MarginLayoutParamsCompat.setMarginEnd(this, dpToPx(DOT_MARGIN))
}
}
)
}
}
/**
* Set the current selected pager dot.
*/
fun setSelection(index: Int) {
if (selectedIndex == index) {
return
}
getChildAt(selectedIndex)?.run {
isSelected = false
}
getChildAt(index)?.run {
isSelected = true
}
selectedIndex = index
}
companion object {
private const val DOT_SIZE_IN_DP = 6f
private const val DOT_MARGIN = 4f
}
}
fun Context.dpToPx(value: Float): Int =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, resources.displayMetrics).toInt()
fun View.dpToPx(value: Float): Int = context.dpToPx(value)

@ -14,6 +14,8 @@ import kotlinx.android.synthetic.main.top_site_item.*
import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import mozilla.components.feature.top.sites.TopSite.Type.DEFAULT
import mozilla.components.feature.top.sites.TopSite.Type.FRECENT
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView import org.mozilla.fenix.ext.loadIntoView
@ -26,23 +28,23 @@ class TopSiteItemViewHolder(
private val interactor: TopSiteInteractor private val interactor: TopSiteInteractor
) : ViewHolder(view) { ) : ViewHolder(view) {
private lateinit var topSite: TopSite private lateinit var topSite: TopSite
private var topSiteMenu: TopSiteItemMenu
init { init {
topSiteMenu = TopSiteItemMenu(view.context) {
when (it) {
is TopSiteItemMenu.Item.OpenInPrivateTab -> interactor.onOpenInPrivateTabClicked(
topSite
)
is TopSiteItemMenu.Item.RemoveTopSite -> interactor.onRemoveTopSiteClicked(topSite)
}
}
top_site_item.setOnClickListener { top_site_item.setOnClickListener {
interactor.onSelectTopSite(topSite.url, topSite.isDefault) interactor.onSelectTopSite(topSite.url, topSite.type === DEFAULT)
} }
top_site_item.setOnLongClickListener { top_site_item.setOnLongClickListener {
val topSiteMenu = TopSiteItemMenu(view.context, topSite.type != FRECENT) { item ->
when (item) {
is TopSiteItemMenu.Item.OpenInPrivateTab -> interactor.onOpenInPrivateTabClicked(
topSite
)
is TopSiteItemMenu.Item.RemoveTopSite -> interactor.onRemoveTopSiteClicked(
topSite
)
}
}
val menu = topSiteMenu.menuBuilder.build(view.context).show(anchor = it) val menu = topSiteMenu.menuBuilder.build(view.context).show(anchor = it)
it.setOnTouchListener @SuppressLint("ClickableViewAccessibility") { v, event -> it.setOnTouchListener @SuppressLint("ClickableViewAccessibility") { v, event ->
onTouchEvent(v, event, menu) onTouchEvent(v, event, menu)
@ -82,6 +84,7 @@ class TopSiteItemViewHolder(
class TopSiteItemMenu( class TopSiteItemMenu(
private val context: Context, private val context: Context,
private val isPinnedSite: Boolean,
private val onItemTapped: (Item) -> Unit = {} private val onItemTapped: (Item) -> Unit = {}
) { ) {
sealed class Item { sealed class Item {
@ -98,9 +101,12 @@ class TopSiteItemMenu(
) { ) {
onItemTapped.invoke(Item.OpenInPrivateTab) onItemTapped.invoke(Item.OpenInPrivateTab)
}, },
SimpleBrowserMenuItem( SimpleBrowserMenuItem(
context.getString(R.string.remove_top_site) if (isPinnedSite) {
context.getString(R.string.remove_top_site)
} else {
context.getString(R.string.delete_from_history)
}
) { ) {
onItemTapped.invoke(Item.RemoveTopSite) onItemTapped.invoke(Item.RemoveTopSite)
} }

@ -0,0 +1,40 @@
/* 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.home.sessioncontrol.viewholders.topsites
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSiteViewHolder
class TopSitesPagerAdapter(
private val interactor: TopSiteInteractor
) : RecyclerView.Adapter<TopSiteViewHolder>() {
private var topSites: List<List<TopSite>> = listOf()
fun updateData(topSites: List<TopSite>) {
this.topSites = topSites.chunked(TOP_SITES_PER_PAGE)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopSiteViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(TopSiteViewHolder.LAYOUT_ID, parent, false)
return TopSiteViewHolder(view, interactor)
}
override fun onBindViewHolder(holder: TopSiteViewHolder, position: Int) {
holder.bind(this.topSites[position])
}
override fun getItemCount(): Int = this.topSites.size
companion object {
const val TOP_SITES_PER_PAGE = 8
}
}

@ -21,6 +21,7 @@ import androidx.navigation.Navigation
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_edit_bookmark.* import kotlinx.android.synthetic.main.fragment_edit_bookmark.*
import kotlinx.android.synthetic.main.fragment_edit_bookmark.view.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -31,6 +32,7 @@ import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.android.view.showKeyboard
import org.mozilla.fenix.NavHostActivity import org.mozilla.fenix.NavHostActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
@ -38,6 +40,7 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.placeCursorAtEnd
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.setToolbarColors import org.mozilla.fenix.ext.setToolbarColors
import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.ext.toShortUrl
@ -107,6 +110,12 @@ class EditBookmarkFragment : Fragment(R.layout.fragment_edit_bookmark) {
) )
} }
} }
view.bookmarkNameEdit.apply {
requestFocus()
placeCursorAtEnd()
showKeyboard()
}
} }
} }

@ -8,6 +8,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.updatePaddingRelative
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -86,7 +87,7 @@ class SelectBookmarkFolderAdapter(private val sharedViewModel: BookmarksSharedVi
} }
val pxToIndent = dpsToIndent.dpToPx(view.context.resources.displayMetrics) val pxToIndent = dpsToIndent.dpToPx(view.context.resources.displayMetrics)
val padding = pxToIndent * if (folder.depth > maxDepth) maxDepth else folder.depth val padding = pxToIndent * if (folder.depth > maxDepth) maxDepth else folder.depth
view.setPadding(padding, 0, 0, 0) view.updatePaddingRelative(start = padding)
} }
companion object { companion object {

@ -0,0 +1,40 @@
/* 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.library.downloads
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.library.SelectionHolder
import org.mozilla.fenix.library.downloads.viewholders.DownloadsListItemViewHolder
class DownloadAdapter(
private val downloadInteractor: DownloadInteractor
) : RecyclerView.Adapter<DownloadsListItemViewHolder>(), SelectionHolder<DownloadItem> {
private var downloads: List<DownloadItem> = listOf()
private var mode: DownloadFragmentState.Mode = DownloadFragmentState.Mode.Normal
override val selectedItems get() = mode.selectedItems
override fun getItemCount(): Int = downloads.size
override fun getItemViewType(position: Int): Int = DownloadsListItemViewHolder.LAYOUT_ID
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadsListItemViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return DownloadsListItemViewHolder(view, downloadInteractor, this)
}
fun updateMode(mode: DownloadFragmentState.Mode) {
this.mode = mode
}
override fun onBindViewHolder(holder: DownloadsListItemViewHolder, position: Int) {
holder.bind(downloads[position])
}
fun updateDownloads(downloads: List<DownloadItem>) {
this.downloads = downloads
notifyDataSetChanged()
}
}

@ -0,0 +1,30 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.downloads
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
interface DownloadController {
fun handleOpen(item: DownloadItem, mode: BrowsingMode? = null)
fun handleBackPressed(): Boolean
}
class DefaultDownloadController(
private val store: DownloadFragmentStore,
private val openToFileManager: (item: DownloadItem, mode: BrowsingMode?) -> Unit
) : DownloadController {
override fun handleOpen(item: DownloadItem, mode: BrowsingMode?) {
openToFileManager(item, mode)
}
override fun handleBackPressed(): Boolean {
return if (store.state.mode is DownloadFragmentState.Mode.Editing) {
store.dispatch(DownloadFragmentAction.ExitEditMode)
true
} else {
false
}
}
}

@ -0,0 +1,113 @@
/* 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.library.downloads
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_downloads.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.feature.downloads.AbstractFetchDownloadService
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.filterNotExistsOnDisk
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.library.LibraryPageFragment
@SuppressWarnings("TooManyFunctions", "LargeClass")
class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHandler {
private lateinit var downloadStore: DownloadFragmentStore
private lateinit var downloadView: DownloadView
private lateinit var downloadInteractor: DownloadInteractor
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_downloads, container, false)
val items = requireComponents.core.store.state.downloads.map {
DownloadItem(
it.value.id.toString(),
it.value.fileName,
it.value.filePath,
it.value.contentLength.toString(),
it.value.contentType,
it.value.status
)
}.filter {
it.status == DownloadState.Status.COMPLETED
}.filterNotExistsOnDisk()
downloadStore = StoreProvider.get(this) {
DownloadFragmentStore(
DownloadFragmentState(
items = items,
mode = DownloadFragmentState.Mode.Normal
)
)
}
val downloadController: DownloadController = DefaultDownloadController(
downloadStore,
::openItem
)
downloadInteractor = DownloadInteractor(
downloadController
)
downloadView = DownloadView(view.downloadsLayout, downloadInteractor)
return view
}
override val selectedItems get() = downloadStore.state.mode.selectedItems
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireComponents.analytics.metrics.track(Event.HistoryOpened)
setHasOptionsMenu(false)
}
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
consumeFrom(downloadStore) {
downloadView.update(it)
}
}
override fun onResume() {
super.onResume()
showToolbar(getString(R.string.library_downloads))
}
override fun onBackPressed(): Boolean {
return downloadView.onBackPressed()
}
private fun openItem(item: DownloadItem, mode: BrowsingMode? = null) {
mode?.let { (activity as HomeActivity).browsingModeManager.mode = it }
context?.let {
AbstractFetchDownloadService.openFile(
context = it,
contentType = item.contentType,
filePath = item.filePath
)
}
}
}

@ -0,0 +1,69 @@
/* 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.library.downloads
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
/**
* Class representing a history entry
* @property id Unique id of the download item
* @property fileName File name of the download item
* @property filePath Full path of the download item
* @property size The size in bytes of the download item
* @property contentType The type of file the download is
*/
data class DownloadItem(
val id: String,
val fileName: String?,
val filePath: String,
val size: String,
val contentType: String?,
val status: DownloadState.Status
)
/**
* The [Store] for holding the [DownloadFragmentState] and applying [DownloadFragmentAction]s.
*/
class DownloadFragmentStore(initialState: DownloadFragmentState) :
Store<DownloadFragmentState, DownloadFragmentAction>(initialState, ::downloadStateReducer)
/**
* Actions to dispatch through the `DownloadStore` to modify `DownloadState` through the reducer.
*/
sealed class DownloadFragmentAction : Action {
object ExitEditMode : DownloadFragmentAction()
}
/**
* The state for the Download Screen
* @property items List of DownloadItem to display
* @property mode Current Mode of Download
*/
data class DownloadFragmentState(
val items: List<DownloadItem>,
val mode: Mode
) : State {
sealed class Mode {
open val selectedItems = emptySet<DownloadItem>()
object Normal : Mode()
data class Editing(override val selectedItems: Set<DownloadItem>) : DownloadFragmentState.Mode()
}
}
/**
* The DownloadState Reducer.
*/
private fun downloadStateReducer(
state: DownloadFragmentState,
action: DownloadFragmentAction
): DownloadFragmentState {
return when (action) {
is DownloadFragmentAction.ExitEditMode -> state.copy(mode = DownloadFragmentState.Mode.Normal)
}
}

@ -0,0 +1,25 @@
/* 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.library.downloads
/**
* Interactor for the download screen
* Provides implementations for the DownloadViewInteractor
*/
@SuppressWarnings("TooManyFunctions")
class DownloadInteractor(
private val downloadController: DownloadController
) : DownloadViewInteractor {
override fun open(item: DownloadItem) {
downloadController.handleOpen(item)
}
override fun select(item: DownloadItem) { /* noop */ }
override fun deselect(item: DownloadItem) { /* noop */ }
override fun onBackPressed(): Boolean {
return downloadController.handleBackPressed()
}
}

@ -0,0 +1,83 @@
/* 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.library.downloads
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import kotlinx.android.synthetic.main.component_downloads.*
import kotlinx.android.synthetic.main.component_downloads.view.*
import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibraryPageView
import org.mozilla.fenix.library.SelectionInteractor
/**
* Interface for the DownloadViewInteractor. This interface is implemented by objects that want
* to respond to user interaction on the DownloadView
*/
interface DownloadViewInteractor : SelectionInteractor<DownloadItem> {
/**
* Called on backpressed to exit edit mode
*/
fun onBackPressed(): Boolean
}
/**
* View that contains and configures the Downloads List
*/
class DownloadView(
container: ViewGroup,
val interactor: DownloadInteractor
) : LibraryPageView(container), UserInteractionHandler {
val view: View = LayoutInflater.from(container.context)
.inflate(R.layout.component_downloads, container, true)
var mode: DownloadFragmentState.Mode = DownloadFragmentState.Mode.Normal
private set
val downloadAdapter = DownloadAdapter(interactor)
private val layoutManager = LinearLayoutManager(container.context)
init {
view.download_list.apply {
layoutManager = this@DownloadView.layoutManager
adapter = downloadAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
}
fun update(state: DownloadFragmentState) {
view.swipe_refresh.isEnabled = false
mode = state.mode
updateEmptyState(state.items.isNotEmpty())
downloadAdapter.updateMode(state.mode)
downloadAdapter.updateDownloads(state.items)
setUiForNormalMode(
context.getString(R.string.library_downloads)
)
}
fun updateEmptyState(userHasDownloads: Boolean) {
download_list.isVisible = userHasDownloads
download_empty_view.isVisible = !userHasDownloads
if (!userHasDownloads) {
download_empty_view.announceForAccessibility(context.getString(R.string.download_empty_message))
}
}
override fun onBackPressed(): Boolean {
return interactor.onBackPressed()
}
}

@ -0,0 +1,47 @@
/* 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.library.downloads.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.download_list_item.view.*
import kotlinx.android.synthetic.main.library_site_item.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.library.SelectionHolder
import org.mozilla.fenix.library.downloads.DownloadInteractor
import org.mozilla.fenix.library.downloads.DownloadItem
import mozilla.components.feature.downloads.toMegabyteString
import org.mozilla.fenix.ext.getIcon
class DownloadsListItemViewHolder(
view: View,
private val downloadInteractor: DownloadInteractor,
private val selectionHolder: SelectionHolder<DownloadItem>
) : RecyclerView.ViewHolder(view) {
private var item: DownloadItem? = null
fun bind(
item: DownloadItem
) {
itemView.download_layout.visibility = View.VISIBLE
itemView.download_layout.titleView.text = item.fileName
itemView.download_layout.urlView.text = item.size.toLong().toMegabyteString()
itemView.download_layout.setSelectionInteractor(item, selectionHolder, downloadInteractor)
itemView.download_layout.changeSelected(item in selectionHolder.selectedItems)
itemView.overflow_menu.hideAndDisable()
itemView.favicon.setImageResource(item.getIcon())
itemView.favicon.isClickable = false
this.item = item
}
companion object {
const val LAYOUT_ID = R.layout.download_list_item
}
}

@ -59,7 +59,7 @@ class MigrationProgressActivity : AbstractMigrationProgressActivity() {
override fun onMigrationCompleted(results: MigrationResults) { override fun onMigrationCompleted(results: MigrationResults) {
// Enable clicking the finish button // Enable clicking the finish button
migration_button_text_view.apply { migration_button.apply {
setOnClickListener { setOnClickListener {
AbstractMigrationService.dismissNotification(context) AbstractMigrationService.dismissNotification(context)
@ -78,6 +78,8 @@ class MigrationProgressActivity : AbstractMigrationProgressActivity() {
startActivity(Intent(this@MigrationProgressActivity, HomeActivity::class.java)) startActivity(Intent(this@MigrationProgressActivity, HomeActivity::class.java))
} }
} }
}
migration_button_text_view.apply {
text = getString(R.string.migration_update_app_button, getString(R.string.app_name)) text = getString(R.string.migration_update_app_button, getString(R.string.app_name))
setTextColor(ContextCompat.getColor(context, R.color.white_color)) setTextColor(ContextCompat.getColor(context, R.color.white_color))
} }

@ -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.onboarding
import android.content.Context
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.settings.SupportUtils
class OnboardingController(
private val context: Context
) {
fun handleLearnMoreClicked() {
(context as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getFirefoxAccountSumoUrl(),
newTab = true,
from = BrowserDirection.FromHome
)
}
}

@ -0,0 +1,14 @@
/* 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.onboarding
class OnboardingInteractor(private val onboardingController: OnboardingController) {
/**
* Called when the user clicks the learn more link
* @param url the url the suggestion was providing
*/
fun onLearnMoreClicked() = onboardingController.handleLearnMoreClicked()
}

@ -71,14 +71,14 @@ class OnboardingRadioButton(
val spannableTitle = SpannableString(resources.getString(title)) val spannableTitle = SpannableString(resources.getString(title))
spannableTitle.setTextSize(context, TITLE_TEXT_SIZE) spannableTitle.setTextSize(context, TITLE_TEXT_SIZE)
spannableTitle.setTextColor(context, R.color.primary_state_list_text_color) spannableTitle.setTextColor(context, R.attr.primaryText)
builder.append(spannableTitle) builder.append(spannableTitle)
if (description != 0) { if (description != 0) {
val spannableDescription = SpannableString(resources.getString(description)) val spannableDescription = SpannableString(resources.getString(description))
spannableDescription.setTextSize(context, DESCRIPTION_TEXT_SIZE) spannableDescription.setTextSize(context, DESCRIPTION_TEXT_SIZE)
spannableDescription.setTextColor(context, R.color.secondary_state_list_text_color) spannableDescription.setTextColor(context, R.attr.secondaryText)
builder.append("\n") builder.append("\n")
builder.append(spannableDescription) builder.append(spannableDescription)
} }

@ -0,0 +1,33 @@
/* 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.perf
import android.view.View
import androidx.core.view.doOnPreDraw
import mozilla.components.support.utils.RunWhenReadyQueue
import java.lang.ref.WeakReference
/**
* class for all functionality related to Visual completeness queue
*/
class VisualCompletenessQueue(val queue: RunWhenReadyQueue) {
@Suppress("MagicNumber")
val delay = 5000L
/**
*
* @param containerWeakReference a weak reference to the root view of a view hierarchy. Weak
* reference is to avoid memory leak.
*/
fun attachViewToRunVisualCompletenessQueueLater(containerWeakReference: WeakReference<View>) {
containerWeakReference.get()?.doOnPreDraw {
// This delay is temporary. We are delaying 5 seconds until the performance
// team can locate the real point of visual completeness.
it.postDelayed({
queue.ready()
}, delay)
}
}
}

@ -61,6 +61,10 @@ class DefaultSearchController(
// and open the crash list activity instead. // and open the crash list activity instead.
activity.startActivity(Intent(activity, CrashListActivity::class.java)) activity.startActivity(Intent(activity, CrashListActivity::class.java))
} }
"about:addons" -> {
val directions = SearchFragmentDirections.actionGlobalAddonsManagementFragment()
navController.navigateSafe(R.id.searchFragment, directions)
}
"moz://a" -> openSearchOrUrl(SupportUtils.getMozillaPageUrl(MANIFESTO)) "moz://a" -> openSearchOrUrl(SupportUtils.getMozillaPageUrl(MANIFESTO))
else -> if (url.isNotBlank()) { else -> if (url.isNotBlank()) {
openSearchOrUrl(url) openSearchOrUrl(url)

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

Loading…
Cancel
Save