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
/taskcluster/ /@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
- [ ] **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:
1. click on Show All Checks,
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".
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
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):
- 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
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!
If you want to run **performance tests/benchmarks** in automation or locally:
- 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 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)
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
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_core
implementation Deps.androidx_core_ktx
implementation Deps.androidx_dynamic_animation
implementation Deps.androidx_transition
implementation Deps.androidx_work_ktx
implementation Deps.google_material
implementation Deps.google_flexbox
implementation Deps.lottie
implementation Deps.adjust
@ -482,6 +479,9 @@ dependencies {
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
// Removed pending AndroidX fixes
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.
Let's just ignore issues in the Sentry code since that is a third-party dependency anyways. -->
<ignore path="**/sentry*.jar" />
<!-- Temporary until https://github.com/Kotlin/kotlinx.coroutines/issues/2004 is resolved. -->
<ignore path="**/kotlinx-coroutines-core-*.jar"/>
</issue>
<!-- Lints that don't apply to our translation process -->
<issue id="MissingTranslation" severity="ignore" />

File diff suppressed because it is too large Load Diff

@ -5,7 +5,9 @@
package org.mozilla.fenix.helpers
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiDevice
import org.mozilla.fenix.HomeActivity
/**
@ -16,7 +18,12 @@ import org.mozilla.fenix.HomeActivity
*/
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
@ -26,5 +33,19 @@ class HomeActivityTestRule(initialTouchMode: Boolean = false, launchActivity: Bo
* @param launchActivity See [IntentsTestRule]
*/
class HomeActivityIntentTestRule(initialTouchMode: Boolean = false, launchActivity: Boolean = true) :
IntentsTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity)
class HomeActivityIntentTestRule(
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.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 {
private var callback: ResourceCallback? = null
override fun isIdleNow(): Boolean {
if (recycler.adapter != null && recycler.adapter!!.itemCount > 0) {
if (recycler.adapter != null && recycler.adapter!!.itemCount > minItemCount) {
if (callback != null) {
callback!!.onTransitionToIdle()
}

@ -113,7 +113,7 @@ class BookmarksTest {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
verifyBookmarkedURL(defaultWebPage.url.toString())
@ -121,21 +121,19 @@ class BookmarksTest {
}
}
@Ignore("Intermittent failures: https://github.com/mozilla-mobile/fenix/issues/10911")
@Test
fun createBookmarkFolderTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
clickAddFolderButton()
verifyKeyboardVisible()
addNewFolderName(bookmarksFolderName)
saveNewFolder()
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
clickAddFolderButton()
verifyKeyboardVisible()
addNewFolderName(bookmarksFolderName)
saveNewFolder()
verifyFolderTitle(bookmarksFolderName)
verifyKeyboardHidden()
}
@ -163,7 +161,7 @@ class BookmarksTest {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) {
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
@ -193,7 +191,7 @@ class BookmarksTest {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) {
}.clickCopy {
@ -210,7 +208,7 @@ class BookmarksTest {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) {
}.clickShare {
@ -230,7 +228,7 @@ class BookmarksTest {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) {
}.clickOpenInNewTab {
@ -249,7 +247,7 @@ class BookmarksTest {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) {
}.clickOpenInPrivateTab {
@ -268,9 +266,10 @@ class BookmarksTest {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) {
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
}.clickDelete {
verifyDeleteSnackBarText()
verifyUndoDeleteSnackBarButton()
@ -306,7 +305,7 @@ class BookmarksTest {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(defaultWebPage.url)
@ -336,7 +335,7 @@ class BookmarksTest {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(defaultWebPage.url)
@ -359,7 +358,7 @@ class BookmarksTest {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(defaultWebPage.url)
@ -384,11 +383,12 @@ class BookmarksTest {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(firstWebPage.url)
longTapSelectItem(secondWebPage.url)
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
}
@ -410,7 +410,7 @@ class BookmarksTest {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(defaultWebPage.url)
@ -466,7 +466,7 @@ class BookmarksTest {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list))
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) {
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.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.notificationShade
import java.io.File
/**
@ -92,10 +93,7 @@ class DownloadTest {
}
@Test
@Ignore("Temp disable flakey test - see: https://github.com/mozilla-mobile/fenix/issues/5462")
fun testDownloadNotification() {
homeScreen { }.dismissOnboarding()
val defaultWebPage = TestAssetHelper.getDownloadAsset(mockWebServer)
navigationToolbar {
@ -108,7 +106,13 @@ class DownloadTest {
verifyDownloadPrompt()
}.clickDownload {
verifyDownloadNotificationPopup()
verifyDownloadNotificationShade()
}
mDevice.openNotification()
notificationShade {
verifySystemNotificationExists("Download completed")
}
// close notification shade before the next test
mDevice.pressBack()
}
}

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

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

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

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

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

@ -4,7 +4,6 @@
package org.mozilla.fenix.ui
import androidx.core.net.toUri
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
@ -16,6 +15,7 @@ import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
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.navigationToolbar
@ -57,7 +57,8 @@ class SmokeTest {
}.goBackToWebsite {
}.openTabDrawer {
verifyExistingTabList()
}.openHomeScreen {
}.openNewTab {
}.dismiss {
verifyHomeScreen()
}
}
@ -105,7 +106,8 @@ class SmokeTest {
}.addToFirefoxHome {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openHomeScreen {
}.openNewTab {
}.dismiss {
verifyExistingTopSitesTabs(defaultWebPage.title)
}.openTabDrawer {
}.openTab(defaultWebPage.title) {
@ -131,13 +133,11 @@ class SmokeTest {
verifyUrl(defaultWebPage.url.toString())
}.openTabDrawer {
closeTabViaXButton(defaultWebPage.title)
}.openHomeScreen {
navigationToolbar {
}.enterURLAndEnterToBrowser(youtubeUrl.toUri()) {
verifyBlueDot()
}.openThreeDotMenu {
verifyOpenInAppButton()
}
}.openNewTab {
}.submitQuery(youtubeUrl) {
verifyBlueDot()
}.openThreeDotMenu {
verifyOpenInAppButton()
}
}
@ -184,7 +184,8 @@ class SmokeTest {
}.addToFirefoxHome {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openHomeScreen {
}.openNewTab {
}.dismiss {
togglePrivateBrowsingModeOnOff()
verifyExistingTopSitesTabs(defaultWebPage.title)
togglePrivateBrowsingModeOnOff()
@ -208,13 +209,11 @@ class SmokeTest {
verifyUrl(defaultWebPage.url.toString())
}.openTabDrawer {
closeTabViaXButton(defaultWebPage.title)
}.openHomeScreen {
navigationToolbar {
}.enterURLAndEnterToBrowser(youtubeUrl.toUri()) {
verifyBlueDot()
}.openThreeDotMenu {
verifyOpenInAppButton()
}
}.openNewTab {
}.submitQuery(youtubeUrl) {
verifyBlueDot()
}.openThreeDotMenu {
verifyOpenInAppButton()
}
}
}
@ -239,7 +238,8 @@ class SmokeTest {
verifyUrl("webcompat.com/issues/new")
verifyTabCounter("2")
}.openTabDrawer {
}.openHomeScreen {
}.openNewTab {
}.dismiss {
}.openThreeDotMenu {
}.openSettings {
}.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
fun closeTabTest() {
var genericURLS = TestAssetHelper.getGenericAssets(mockWebServer)
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")
}
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}")
swipeTabRight("Test_Page_${index + 1}")
verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO")
}
navigationToolbar {
}.openNewTabAndEnterToBrowser(genericURL.url) {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
verifyCloseTabsButton("Test_Page_1")
closeTabViaXButton("Test_Page_1")
verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}")
swipeTabLeft("Test_Page_${index + 1}")
verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO")
}
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
swipeTabRight("Test_Page_1")
verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}")
}.openHomeScreen {
}
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
swipeTabLeft("Test_Page_1")
verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
}.openNewTab {
}.dismiss { }
}
@Test
fun closePrivateTabTest() {
var genericURLS = TestAssetHelper.getGenericAssets(mockWebServer)
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen { }.togglePrivateBrowsingMode()
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("Private tab closed")
snackBarButtonClick("UNDO")
}
navigationToolbar {
}.openNewTabAndEnterToBrowser(genericURL.url) {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
verifyCloseTabsButton("Test_Page_1")
closeTabViaXButton("Test_Page_1")
verifySnackBarText("Private tab closed")
snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}")
swipeTabRight("Test_Page_${index + 1}")
verifySnackBarText("Private tab closed")
snackBarButtonClick("UNDO")
}
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
swipeTabRight("Test_Page_1")
verifySnackBarText("Private tab closed")
snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}")
swipeTabLeft("Test_Page_${index + 1}")
verifySnackBarText("Private tab closed")
snackBarButtonClick("UNDO")
}
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
swipeTabLeft("Test_Page_1")
verifySnackBarText("Private tab closed")
snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}")
closeTabViaXButton("Test_Page_${index + 1}")
}
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
}
}
@ -290,8 +285,8 @@ class TabbedBrowsingTest {
verifyTabTrayOverflowMenu(true)
verifyExistingOpenTabs(defaultWebPage.title)
verifyCloseTabsButton(defaultWebPage.title)
}.openHomeScreen {
}
}.openNewTab {
}.dismiss { }
}
@Test

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

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

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

@ -12,12 +12,14 @@ import android.net.Uri
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.BundleMatchers
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.RootMatchers.isDialog
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.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed

@ -37,8 +37,6 @@ class DownloadRobot {
fun verifyDownloadNotificationPopup() = assertDownloadNotificationPopup()
fun verifyDownloadNotificationShade() = assertDownloadNotificationShade()
fun verifyPhotosAppOpens() = assertPhotosOpens()
class Transition {
@ -98,17 +96,6 @@ private fun assertDownloadPrompt() {
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() {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.waitNotNull(Until.findObjects(By.text("Open")), TestAssetHelper.waitingTime)

@ -553,11 +553,11 @@ private fun assertWelcomeHeader() =
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
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)))
private fun assertAccountsSignInButton() =
onView(ViewMatchers.withResourceName("turn_on_sync_button"))
onView(ViewMatchers.withResourceName("fxa_sign_in_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertGetToKnowHeader() =

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

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

@ -6,6 +6,7 @@
package org.mozilla.fenix.ui.robots
import android.widget.ToggleButton
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
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.contrib.RecyclerViewActions
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.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
@ -28,8 +31,11 @@ import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.startsWith
import org.hamcrest.Matchers
import org.junit.Assert.assertEquals
import org.mozilla.fenix.R
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
/**
@ -47,6 +53,24 @@ class SearchRobot {
fun verifySearchSettings() = assertSearchSettings()
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() {
scanButton().perform(click())
}
@ -82,10 +106,10 @@ class SearchRobot {
fun scrollToSearchEngineSettings() {
// 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()
)
onView(allOf(withId(R.id.awesomeBar))).perform(ViewActions.swipeUp())
onView(allOf(withId(R.id.awesome_bar))).perform(ViewActions.swipeUp())
}
fun clickSearchEngineSettings() {
@ -99,6 +123,13 @@ class SearchRobot {
class Transition {
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 {
mDevice.waitForIdle()
browserToolbarEditView().perform(typeText("mozilla\n"))
@ -106,10 +137,23 @@ class SearchRobot {
BrowserRobot().interact()
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() =
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 searchWrapper() = onView(withId(R.id.search_wrapper))
private fun assertSearchEngineURL(searchEngineName: String) {
mDevice.waitNotNull(
Until.findObject(By.textContains("${searchEngineName.toLowerCase()}.com/?q=mozilla")),
@ -178,4 +224,46 @@ fun searchScreen(interact: SearchRobot.() -> Unit): 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")))

@ -222,7 +222,7 @@ private fun assertLibrariesUsed() {
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.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()
}

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

@ -164,7 +164,7 @@ class ThreeDotMenuMainRobot {
fun openBookmarks(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
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()
BookmarksRobot().interact()

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

@ -31,5 +31,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromEditCustomSearchEngineFragment(R.id.editCustomSearchEngineFragment),
FromAddonDetailsFragment(R.id.addonDetailsFragment),
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
/**
* 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 mozilla.appservices.Megazord
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.action.SystemAction
import mozilla.components.concept.push.PushProcessor
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
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.components.Components
import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.resetPoliciesAfter
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StorageStatsMetrics
@ -141,7 +143,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
}
prefetchForHomeFragment()
setupLeakCanary()
startMetricsIfEnabled()
setupPush()
@ -156,18 +157,20 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
// }
initVisualCompletenessQueueAndQueueTasks()
components.appStartupTelemetry.onFenixApplicationOnCreate()
}
private fun initVisualCompletenessQueueAndQueueTasks() {
val taskQueue = components.performance.visualCompletenessQueue
val queue = components.performance.visualCompletenessQueue.queue
fun initQueue() {
registerActivityLifecycleCallbacks(PerformanceActivityLifecycleCallbacks(taskQueue))
registerActivityLifecycleCallbacks(PerformanceActivityLifecycleCallbacks(queue))
}
fun queueInitExperiments() {
if (settings().isExperimentationEnabled) {
taskQueue.runIfReadyOrQueue {
queue.runIfReadyOrQueue {
Experiments.initialize(
applicationContext = applicationContext,
onExperimentsUpdated = {
@ -188,7 +191,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
fun queueInitStorageAndServices() {
components.performance.visualCompletenessQueue.runIfReadyOrQueue {
components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue {
GlobalScope.launch(Dispatchers.IO) {
logger.info("Running post-visual completeness tasks...")
logElapsedTime(logger, "Storage initialization") {
@ -208,7 +211,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
fun queueMetrics() {
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
// create a WorkManager task for this metric, however, I ran out of
// 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()
// 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()
queueInitStorageAndServices()
queueMetrics()
queueReviewPrompt()
}
private fun startMetricsIfEnabled() {
@ -257,14 +267,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
// 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() {
// 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
@ -318,7 +320,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
runOnlyInMainProcess {
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
),
SettingsAddonManager(
NavGraphDirections.actionGlobalSettingsAddonsManagementFragment(),
NavGraphDirections.actionGlobalAddonsManagementFragment(),
R.id.addonsManagementFragment
),
SettingsLogins(

@ -6,6 +6,7 @@ package org.mozilla.fenix
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.os.StrictMode
@ -21,7 +22,6 @@ import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PROTECTED
import androidx.appcompat.app.ActionBar
import androidx.appcompat.widget.Toolbar
import androidx.core.view.doOnPreDraw
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
@ -40,13 +40,11 @@ import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.SessionState
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.EngineView
import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
import mozilla.components.feature.privatemode.notification.PrivateNotificationFeature
import mozilla.components.feature.search.BrowserStoreSearchAdapter
import mozilla.components.feature.search.SearchAdapter
import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.support.base.feature.UserInteractionHandler
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.toNormalizedUrl
import mozilla.components.support.locale.LocaleAwareAppCompatActivity
import mozilla.components.support.utils.RunWhenReadyQueue
import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent
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.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
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.sync.SyncedTabsFragmentDirections
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
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)
@ -121,7 +121,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
private var isVisuallyComplete = false
private var visualCompletenessQueue: RunWhenReadyQueue? = null
private var privateNotificationObserver: PrivateNotificationFeature<PrivateNotificationService>? = null
private var isToolbarInflated = false
@ -156,6 +155,16 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
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()
setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent))
@ -163,13 +172,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// Must be after we set the content view
if (isVisuallyComplete) {
rootContainer.doOnPreDraw {
// This delay is temporary. We are delaying 5 seconds until the performance
// team can locate the real point of visual completeness.
it.postDelayed({
visualCompletenessQueue!!.ready()
}, delay)
}
components.performance.visualCompletenessQueue
.attachViewToRunVisualCompletenessQueueLater(WeakReference(rootContainer))
}
sessionObserver = UriOpenedObserver(this)
@ -226,19 +230,33 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
captureSnapshotTelemetryMetrics()
setAppAllStartTelemetry(intent.toSafeIntent())
startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null)
StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
}
protected open fun setAppAllStartTelemetry(safeIntent: SafeIntent) {
components.appAllSourceStartTelemetry.receivedIntentInHomeActivity(safeIntent)
protected open fun startupTelemetryOnCreateCalled(safeIntent: SafeIntent, hasSavedInstanceState: Boolean) {
components.appStartupTelemetry.onHomeActivityOnCreate(safeIntent, hasSavedInstanceState)
}
override fun onRestart() {
super.onRestart()
components.appStartupTelemetry.onHomeActivityOnRestart()
}
@CallSuper
override fun 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 {
lifecycleScope.launch {
// 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() {
if (settings().lastKnownMode.isPrivate) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
@ -273,6 +314,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
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
// is about to change the browsers installed on their system. Therefore, we reset the cache of
// all the installed browsers.
@ -283,9 +333,39 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
override fun 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()
}
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.
*/
@ -293,6 +373,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
super.onNewIntent(intent)
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 =
listOf(CrashReporterIntentProcessor()) + externalSourceIntentProcessors
val intentHandled =
@ -317,7 +406,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
.let(::getIntentAllSource)
?.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) {
EngineView::class.java.name -> components.core.engine.createView(context, attrs).apply {
selectionActionDelegate = DefaultSelectionActionDelegate(
getSearchAdapter(components.core.store),
BrowserStoreSearchAdapter(
components.core.store,
tabId = getIntentSessionId(intent.toSafeIntent())
),
resources = context.resources,
shareTextClicked = { share(it) },
emailTextClicked = { email(it) },
@ -424,9 +516,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
super.onUserLeaveHint()
}
protected open fun getSearchAdapter(store: BrowserStore): SearchAdapter =
BrowserStoreSearchAdapter(store)
protected open fun getBreadcrumbMessage(destination: NavDestination): String {
val fragmentName = resources.getResourceEntryName(destination.id)
return "Changing to fragment $fragmentName, isCustomTab: false"
@ -597,6 +686,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromLoginDetailFragment ->
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
* we are visually complete.
*/
fun postVisualCompletenessQueue(visualCompletenessQueue: RunWhenReadyQueue) {
fun setVisualCompletenessQueueReady() {
isVisuallyComplete = true
this.visualCompletenessQueue = visualCompletenessQueue
}
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 EXTRA_DELETE_PRIVATE_TABS = "notification_delete_and_open"
const val EXTRA_OPENED_FROM_NOTIFICATION = "notification_open"
const val delay = 5000L
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

@ -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) {
val managementView = AddonsManagementView(
navController = findNavController(),
@ -120,7 +126,6 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
addonNameTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context),
addonSummaryTextColor = ThemeManager.resolveAttribute(R.attr.secondaryText, context),
sectionsTypeFace = ResourcesCompat.getFont(context, R.font.metropolis_semibold),
addonBackgroundIconColor = ThemeManager.resolveAttribute(R.attr.inset, requireContext()),
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.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.accounts.FxaCapability
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.share.ShareDelegate
import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.feature.search.SearchFeature
import mozilla.components.feature.session.FullScreenFeature
import mozilla.components.feature.session.PictureInPictureFeature
import mozilla.components.feature.session.SessionFeature
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.session.SwipeRefreshFeature
import mozilla.components.feature.session.behavior.EngineViewBottomBehavior
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.ext.getPreferenceKey
import org.mozilla.fenix.ext.accessibilityManager
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.enterToImmersiveMode
import org.mozilla.fenix.ext.getPreferenceKey
@ -133,6 +135,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
get() = _browserToolbarView!!
protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
protected val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>()
@ -150,6 +153,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
private var fullScreenMediaFeature =
ViewBoundFeatureWrapper<MediaFullscreenOrientationFeature>()
private val searchFeature = ViewBoundFeatureWrapper<SearchFeature>()
private var pipFeature: PictureInPictureFeature? = null
var customTabSessionId: String? = null
@ -169,11 +173,16 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
require(arguments != null)
customTabSessionId = arguments?.getString(EXTRA_SESSION_ID)
val view = if (FeatureFlags.browserChromeGestures) {
inflater.inflate(R.layout.browser_gesture_wrapper, container, false)
} else {
inflater.inflate(R.layout.fragment_browser, container, false)
}
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
message = "onCreateView()",
data = mapOf(
"customTabSessionId" to customTabSessionId.toString()
)
)
val view = inflater.inflate(R.layout.fragment_browser, container, false)
val activity = activity as HomeActivity
activity.themeManager.applyStatusBarTheme(activity)
@ -212,6 +221,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, 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(
activity = requireActivity() as HomeActivity,
navController = findNavController(),
@ -227,15 +241,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
swipeRefresh = swipeRefresh,
browserAnimator = browserAnimator,
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply {
action = Intent.ACTION_VIEW
putExtra(HomeActivity.OPEN_TO_BROWSER, true)
},
openInFenixIntent = openInFenixIntent,
bookmarkTapped = { viewLifecycleOwner.lifecycleScope.launch { bookmarkTapped(it) } },
scope = viewLifecycleOwner.lifecycleScope,
tabCollectionStorage = requireComponents.core.tabCollectionStorage,
topSiteStorage = requireComponents.core.topSiteStorage,
onTabCounterClicked = {
thumbnailsFeature.get()?.requestScreenshot()
findNavController().nav(
R.id.browserFragment,
BrowserFragmentDirections.actionGlobalTabTrayDialogFragment()
@ -477,7 +488,16 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
},
onNeedToRequestPermissions = { 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,
view = view
)
@ -486,7 +506,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
feature = SessionFeature(
requireComponents.core.store,
requireComponents.useCases.sessionUseCases.goBack,
requireComponents.useCases.engineSessionUseCases,
view.engineView,
customTabSessionId
),
@ -494,6 +513,26 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
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 =
ThemeManager.resolveAttribute(R.attr.accentHighContrast, context)
@ -541,7 +580,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
fullScreenFeature.set(
feature = FullScreenFeature(
requireComponents.core.store,
SessionUseCases(sessionManager),
requireComponents.useCases.sessionUseCases,
customTabSessionId,
::viewportFitChange,
::fullScreenChanged
@ -593,7 +632,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
if (showEngineView) {
engineView?.asView()?.isVisible = true
swipeRefresh.alpha = 1f
swipeRefresh?.alpha = 1f
} else {
engineView?.asView()?.isVisible = false
}
@ -1063,11 +1102,38 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
*/
override fun 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)
_browserToolbarView = 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 {
private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id"
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1

@ -15,7 +15,6 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
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.view.*
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.contextmenu.ContextMenuCandidate
import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.feature.search.SearchFeature
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.WindowFeature
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.addons.runIfFragmentIsAttached
import org.mozilla.fenix.components.FenixSnackbar
@ -55,8 +52,6 @@ import org.mozilla.fenix.trackingprotection.TrackingProtectionOverlay
class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val searchFeature = ViewBoundFeatureWrapper<SearchFeature>()
private val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
private var readerModeAvailable = false
@ -77,19 +72,15 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
val components = context.components
return super.initializeUI(view)?.also {
// We need to wrap this whole thing in an if here because gestureLayout will not exist
// if the feature flag is off
if (FeatureFlags.browserChromeGestures) {
gestureLayout.addGestureListener(
ToolbarGestureHandler(
activity = requireActivity(),
contentLayout = browserLayout,
tabPreview = tabPreview,
toolbarLayout = browserToolbarView.view,
sessionManager = components.core.sessionManager
)
gestureLayout.addGestureListener(
ToolbarGestureHandler(
activity = requireActivity(),
contentLayout = browserLayout,
tabPreview = tabPreview,
toolbarLayout = browserToolbarView.view,
sessionManager = components.core.sessionManager
)
}
)
val readerModeAction =
BrowserToolbar.ToggleButton(
@ -148,23 +139,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
owner = this,
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.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.app.Activity
import android.graphics.PointF
import android.graphics.Rect
import android.util.TypedValue
import android.view.View
import android.view.ViewConfiguration
import androidx.annotation.Dimension
import androidx.annotation.Dimension.DP
import androidx.core.animation.doOnEnd
import androidx.core.graphics.contains
import androidx.core.graphics.toPoint
import androidx.core.view.isVisible
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FlingAnimation
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.support.ktx.android.util.dpToPx
@ -61,11 +61,6 @@ class ToolbarGestureHandler(
private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop
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
@ -143,25 +138,12 @@ class ToolbarGestureHandler(
) {
val destination = getDestination()
if (destination is Destination.Tab && isGestureComplete(velocityX)) {
animateToNextTab(velocityX, destination.session)
animateToNextTab(destination.session)
} else {
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 {
val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
val currentSession = sessionManager.selectedSession ?: return Destination.None
@ -234,73 +216,59 @@ class ToolbarGestureHandler(
abs(velocityX) >= minimumFlingVelocity)
}
private fun getVelocityFromFling(velocityX: Float): Float {
return max(abs(velocityX), defaultVelocity)
private fun getAnimator(finalContextX: Float, duration: Long): ValueAnimator {
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) {
GestureDirection.RIGHT_TO_LEFT -> -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
createFlingAnimation(
view = contentLayout,
minValue = min(0f, browserFinalXCoordinate),
maxValue = max(0f, browserFinalXCoordinate),
startVelocity = animationVelocity
).addUpdateListener { _, value, _ ->
tabPreview.translationX = when (gestureDirection) {
GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset
GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset
getAnimator(browserFinalXCoordinate, FINISHED_GESTURE_ANIMATION_DURATION).apply {
doOnEnd {
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
}
})
}
}.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()
}
private fun animateCanceledGesture(gestureVelocity: Float) {
val velocity = if (getDestination() is Destination.None) {
defaultVelocity
private fun animateCanceledGesture(velocityX: Float) {
val duration = if (abs(velocityX) >= minimumFlingVelocity) {
CANCELED_FLING_ANIMATION_DURATION
} else {
getVelocityFromFling(gestureVelocity)
}.let { v ->
when (gestureDirection) {
GestureDirection.RIGHT_TO_LEFT -> v
GestureDirection.LEFT_TO_RIGHT -> -v
}
CANCELED_GESTURE_ANIMATION_DURATION
}
createFlingAnimation(
view = contentLayout,
minValue = min(0f, contentLayout.translationX),
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
getAnimator(0f, duration).apply {
doOnEnd {
tabPreview.isVisible = false
}
}.addEndListener { _, _, _, _ ->
tabPreview.isVisible = false
}.start()
}
@ -337,15 +305,24 @@ class ToolbarGestureHandler(
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)
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 PREVIEW_OFFSET = 48
private const val FINISHED_GESTURE_ANIMATION_DURATION = 250L
/**
* 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.support.utils.RunWhenReadyQueue
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
@ -85,11 +84,8 @@ class BackgroundServices(
)
@VisibleForTesting
val supportedEngines = if (FeatureFlags.syncedTabs) {
val supportedEngines =
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
init {
@ -98,10 +94,7 @@ class BackgroundServices(
GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarkStorage)
GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to passwordsStorage)
if (FeatureFlags.syncedTabs) {
GlobalSyncableStoreProvider.configureStore(SyncEngine.Tabs to remoteTabsStorage)
}
GlobalSyncableStoreProvider.configureStore(SyncEngine.Tabs to remoteTabsStorage)
}
private val telemetryAccountObserver = TelemetryAccountObserver(

@ -18,7 +18,7 @@ import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.migration.state.MigrationStore
import org.mozilla.fenix.BuildConfig
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.Mockable
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 core by lazy { Core(context) }
val core by lazy { Core(context, analytics.crashReporter) }
val search by lazy { Search(context) }
val useCases by lazy {
UseCases(
@ -53,7 +53,8 @@ class Components(private val context: Context) {
core.sessionManager,
core.store,
search.searchEngineManager,
core.webAppShortcutManager
core.webAppShortcutManager,
core.topSiteStorage
)
}
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")
val addonUpdater by lazy {
@ -114,4 +115,11 @@ class Components(private val context: Context) {
val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context.getSystemService()!!) }
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 android.content.Context
import android.content.res.Configuration
import android.os.StrictMode
import io.sentry.Sentry
import kotlinx.coroutines.Dispatchers
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.fetch.GeckoViewFetchClient
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.engine.EngineMiddleware
import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.state.state.BrowserState
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.readerview.ReaderViewMiddleware
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.reporter.WebCompatReporterFeature
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.StatementRelationChecker
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.Config
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.resetPoliciesAfter
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.media.MediaService
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
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 java.util.concurrent.TimeUnit
@ -63,7 +73,7 @@ import java.util.concurrent.TimeUnit
* Component group for all core browser functionality.
*/
@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
* configuration (see build variants).
@ -134,10 +144,14 @@ class Core(private val context: Context) {
DownloadMiddleware(context, DownloadService::class.java),
ReaderViewMiddleware(),
ThumbnailsMiddleware(thumbnailStorage)
)
) + EngineMiddleware.create(engine, ::findSessionById)
)
}
private fun findSessionById(tabId: String): Session? {
return sessionManager.findSessionById(tabId)
}
/**
* 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
// the app is used.
sessionStorage.autoSave(sessionManager)
sessionStorage.autoSave(store)
.periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
.whenGoingToBackground()
.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.
// For example, this is how the GeckoEngine delegates (history, logins) are configured.
// 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 lazyPasswordsStorage = lazy { SyncableLoginsStorage(context, passwordsEncryptionKey) }
@ -249,7 +263,46 @@ class Core(private val context: 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) }
@ -273,7 +326,8 @@ class Core(private val context: Context) {
getSecureAbove22Preferences().getString(PASSWORDS_KEY)
?: generateEncryptionKey(KEY_STRENGTH).also {
if (context.settings().passwordsEncryptionKeyGenerated &&
isSentryEnabled()) {
isSentryEnabled()
) {
// 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")
}
@ -290,7 +344,7 @@ class Core(private val context: Context) {
fun getPreferredColorScheme(): PreferredColorScheme {
val inDark =
(context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES
Configuration.UI_MODE_NIGHT_YES
return when {
context.settings().shouldUseDarkTheme -> PreferredColorScheme.Dark
context.settings().shouldUseLightTheme -> PreferredColorScheme.Light

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

@ -5,10 +5,11 @@
package org.mozilla.fenix.components
import mozilla.components.support.utils.RunWhenReadyQueue
import org.mozilla.fenix.perf.VisualCompletenessQueue
/**
* Component group for all functionality related to performance.
*/
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 mozilla.components.browser.search.SearchEngineManager
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.usecases.EngineSessionUseCases
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
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.TrackingProtectionUseCases
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
/**
@ -34,12 +35,13 @@ class UseCases(
private val sessionManager: SessionManager,
private val store: BrowserStore,
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.
*/
val sessionUseCases by lazy { SessionUseCases(sessionManager) }
val sessionUseCases by lazy { SessionUseCases(store, sessionManager) }
/**
* Use cases that provide tab management.
@ -49,7 +51,7 @@ class UseCases(
/**
* 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.
@ -66,7 +68,10 @@ class UseCases(
val contextMenuUseCases by lazy { ContextMenuUseCases(store) }
val engineSessionUseCases by lazy { EngineSessionUseCases(sessionManager) }
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 SupportTapped : Event()
object PrivacyNoticeTapped : Event()
object RightsTapped : Event()
object LicensingTapped : Event()
object LibrariesThatWeUseTapped : Event()
object PocketTopSiteClicked : Event()
object PocketTopSiteRemoved : Event()
object FennecToFenixMigrated : Event()
@ -319,11 +316,28 @@ sealed class Event {
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 Type { COLD, WARM, HOT, ERROR }
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() {
@ -488,7 +502,7 @@ sealed class Event {
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,
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>?

@ -106,7 +106,7 @@ private val Event.wrapper: EventWrapper<*>?
{ Events.appReceivedIntent.record(it) },
{ Events.appReceivedIntentKeys.valueOf(it) }
)
is Event.AppOpenedAllSourceStartup -> EventWrapper(
is Event.AppAllStartup -> EventWrapper(
{ Events.appOpenedAllStartup.record(it) },
{ Events.appOpenedAllStartupKeys.valueOf(it) }
)
@ -531,15 +531,6 @@ private val Event.wrapper: EventWrapper<*>?
is Event.PrivacyNoticeTapped -> EventWrapper<NoExtraKeys>(
{ 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>(
{ 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
// 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
// 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.

@ -7,6 +7,7 @@ package org.mozilla.fenix.components.metrics
import android.app.Application
import android.content.Context.MODE_PRIVATE
import android.net.Uri
import android.os.StrictMode
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.leanplum.Leanplum
@ -22,6 +23,7 @@ import kotlinx.coroutines.withContext
import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.components.metrics.MozillaProductDetector.MozillaProducts
import org.mozilla.fenix.ext.resetPoliciesAfter
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor
import java.util.Locale
@ -81,7 +83,9 @@ class LeanplumMetricsService(
override val type = MetricServiceType.Marketing
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
internal val deviceId by lazy {

@ -5,6 +5,7 @@
package org.mozilla.fenix.components.metrics
import androidx.annotation.VisibleForTesting
import com.leanplum.Leanplum
import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts
import mozilla.components.browser.menu.facts.BrowserMenuFacts
import mozilla.components.browser.toolbar.facts.ToolbarFacts
@ -193,6 +194,7 @@ internal class ReleaseMetricController(
if (installedAddons is List<*>) {
Addons.installedAddons.set(installedAddons.map { it.toString() })
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<*>) {
Addons.enabledAddons.set(enabledAddons.map { it.toString() })
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.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
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.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.TopSiteStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
@ -75,13 +75,12 @@ class DefaultBrowserToolbarController(
private val bookmarkTapped: (Session) -> Unit,
private val scope: CoroutineScope,
private val tabCollectionStorage: TabCollectionStorage,
private val topSiteStorage: TopSiteStorage,
private val onTabCounterClicked: () -> Unit,
private val onCloseTab: (Session) -> Unit
) : BrowserToolbarController {
private val useNewSearchExperience
get() = activity.settings().useNewSearchExperience
get() = FeatureFlags.newSearchExperience
private val currentSession
get() = customTabSession ?: activity.components.core.sessionManager.selectedSession
@ -245,7 +244,9 @@ class DefaultBrowserToolbarController(
scope.launch {
ioScope.launch {
currentSession?.let {
topSiteStorage.addTopSite(it.title, it.url)
with(activity.components.useCases.topSitesUseCase) {
addPinnedSites(it.title, it.url)
}
}
}.join()
@ -380,6 +381,13 @@ class DefaultBrowserToolbarController(
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.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS
ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY
ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS
}
activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem))

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

@ -12,6 +12,7 @@ import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater
import android.widget.RelativeLayout
import androidx.core.view.updatePadding
import kotlinx.android.synthetic.main.mozac_ui_tabcounter_layout.view.*
import org.mozilla.fenix.R
import java.text.NumberFormat
@ -178,7 +179,7 @@ class TabCounter @JvmOverloads constructor(
private fun formatForDisplay(count: Int): String {
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
} else NumberFormat.getInstance().format(count.toLong())
}

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

@ -4,19 +4,15 @@
package org.mozilla.fenix.customtabs
import android.content.Intent
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import mozilla.components.browser.session.runWithSession
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.manifest.WebAppManifestParser
import mozilla.components.feature.intent.ext.getSessionId
import mozilla.components.feature.pwa.ext.getWebAppManifest
import mozilla.components.feature.search.SearchAdapter
import mozilla.components.support.utils.SafeIntent
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
@ -28,12 +24,6 @@ import java.security.InvalidParameterException
*/
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 {
val fragmentName = resources.getResourceEntryName(destination.id)
return "Changing to fragment $fragmentName, isCustomTab: true"
@ -45,8 +35,8 @@ open class ExternalAppBrowserActivity : HomeActivity() {
final override fun getIntentSessionId(intent: SafeIntent) = intent.getSessionId()
override fun setAppAllStartTelemetry(safeIntent: SafeIntent) {
components.appAllSourceStartTelemetry.receivedIntentInExternalAppBrowserActivity(safeIntent)
override fun startupTelemetryOnCreateCalled(safeIntent: SafeIntent, hasSavedInstanceState: Boolean) {
components.appStartupTelemetry.onExternalAppBrowserOnCreate(safeIntent, hasSavedInstanceState)
}
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() {
super.onDestroy()

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

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

@ -4,39 +4,41 @@
package org.mozilla.fenix.exceptions.viewholders
import android.view.View
import kotlinx.android.synthetic.main.exception_item.*
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.ui.widgets.WidgetSiteItemView
import org.mozilla.fenix.R
import org.mozilla.fenix.exceptions.ExceptionsInteractor
import org.mozilla.fenix.ext.components
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.
*/
class ExceptionsListItemViewHolder<T : Any>(
view: View,
private val view: WidgetSiteItemView,
private val interactor: ExceptionsInteractor<T>,
private val icons: BrowserIcons = view.context.components.core.icons
) : ViewHolder(view) {
) : RecyclerView.ViewHolder(view) {
private lateinit var item: T
init {
delete_exception.setOnClickListener {
view.setSecondaryButton(
icon = R.drawable.ic_close,
contentDescription = R.string.history_delete_item
) {
interactor.onDeleteOne(item)
}
}
fun bind(item: T, url: String) {
this.item = item
webAddressView.text = url
icons.loadIntoView(favicon_image, url)
view.setText(label = url, caption = null)
icons.loadIntoView(view.iconView, url)
}
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.view.View
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.
@ -22,3 +23,19 @@ fun Activity.enterToImmersiveMode() {
or View.SYSTEM_UI_FLAG_FULLSCREEN
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.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.view.ContextThemeWrapper
import android.view.View
import android.view.ViewGroup
@ -89,3 +92,21 @@ fun Context.getStringWithArgSafe(@StringRes resId: Int, formatArg: String): Stri
*/
val Context.accessibilityManager: AccessibilityManager get() =
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.fragment.NavHostFragment.findNavController
import androidx.navigation.fragment.findNavController
import mozilla.components.support.base.crash.Breadcrumb
import org.mozilla.fenix.NavHostActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components
@ -57,3 +58,23 @@ fun Fragment.redirectToReAuth(destinations: List<Int>, currentDestination: Int?)
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.style.AbsoluteSizeSpan
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
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) =
this.setSpan(
ForegroundColorSpan(
ContextCompat.getColor(
context,
colorResId
)
context.getColorFromAttr(colorResId)
),
0,
this.length,

@ -16,7 +16,6 @@ import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.widget.Button
import android.widget.LinearLayout
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.OAuthAccount
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.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
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.SessionControlView
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.settings.SupportUtils
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
@ -146,8 +149,11 @@ class HomeFragment : Fragment() {
private val store: BrowserStore
get() = requireComponents.core.store
private val onboarding by lazy { StrictMode.allowThreadDiskReads().resetPoliciesAfter {
FenixOnboarding(requireContext()) } }
private val onboarding by lazy {
StrictMode.allowThreadDiskReads().resetPoliciesAfter {
FenixOnboarding(requireContext())
}
}
private lateinit var homeFragmentStore: HomeFragmentStore
private var _sessionControlInteractor: SessionControlInteractor? = null
@ -157,6 +163,8 @@ class HomeFragment : Fragment() {
private var sessionControlView: SessionControlView? = null
private lateinit var currentMode: CurrentMode
private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
@ -190,22 +198,31 @@ class HomeFragment : Fragment() {
collections = components.core.tabCollectionStorage.cachedTabCollections,
expandedCollections = emptySet(),
mode = currentMode.getCurrentMode(),
topSites = StrictMode.allowThreadDiskReads().resetPoliciesAfter {
components.core.topSiteStorage.cachedTopSites
},
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip()
topSites = components.core.topSiteStorage.cachedTopSites,
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip(),
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
)
)
}
topSitesFeature.set(
feature = TopSitesFeature(
view = DefaultTopSitesView(homeFragmentStore),
storage = components.core.topSiteStorage,
config = ::getTopSitesConfig
),
owner = this,
view = view
)
_sessionControlInteractor = SessionControlInteractor(
DefaultSessionControlController(
activity = activity,
settings = components.settings,
engine = components.core.engine,
metrics = components.analytics.metrics,
sessionManager = sessionManager,
tabCollectionStorage = components.core.tabCollectionStorage,
topSiteStorage = components.core.topSiteStorage,
addTabUseCase = components.useCases.tabsUseCases.addTab,
fragmentStore = homeFragmentStore,
navController = findNavController(),
@ -220,9 +237,9 @@ class HomeFragment : Fragment() {
updateLayout(view)
sessionControlView = SessionControlView(
view.sessionControlRecyclerView,
viewLifecycleOwner,
sessionControlInteractor,
homeViewModel,
requireComponents.core.store.state.normalTabs.isNotEmpty()
homeViewModel
)
updateSessionControlView(view)
@ -231,6 +248,15 @@ class HomeFragment : Fragment() {
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
* [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 {
ToolbarPopupWindow.show(
WeakReference(view),
WeakReference(it),
handlePasteAndGo = sessionControlInteractor::onPasteAndGo,
handlePaste = sessionControlInteractor::onPaste,
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
// the CFR in.
view.toolbar_wrapper.doOnLayout {
if (!browsingModeManager.mode.isPrivate) {
val willNavigateToSearch = !bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience
if (!browsingModeManager.mode.isPrivate && !willNavigateToSearch) {
SearchWidgetCFR(
context = view.context,
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) {
requireActivity().window.addFlags(FLAG_SECURE)
} else {
@ -418,7 +432,7 @@ class HomeFragment : Fragment() {
updateTabCounter(requireComponents.core.store.state)
if (args.focusOnAddressBar && requireContext().settings().useNewSearchExperience) {
if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience) {
navigateToSearch()
}
}
@ -506,7 +520,6 @@ class HomeFragment : Fragment() {
override fun onStart() {
super.onStart()
subscribeToTabCollections()
subscribeToTopSites()
val context = requireContext()
val components = context.components
@ -516,7 +529,8 @@ class HomeFragment : Fragment() {
collections = components.core.tabCollectionStorage.cachedTabCollections,
mode = currentMode.getCurrentMode(),
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
requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver)
lifecycleScope.launch(IO) {
requireComponents.reviewPromptController.promptReview(requireActivity())
}
}
private fun dispatchModeChanges(mode: Mode) {
@ -679,7 +697,7 @@ class HomeFragment : Fragment() {
}
private fun navigateToSearch() {
val directions = if (requireContext().settings().useNewSearchExperience) {
val directions = if (FeatureFlags.newSearchExperience) {
HomeFragmentDirections.actionGlobalSearchDialog(
sessionId = null
)
@ -771,6 +789,15 @@ class HomeFragment : Fragment() {
HomeFragmentDirections.actionGlobalHistoryFragment()
)
}
HomeMenu.Item.Downloads -> {
hideOnboardingIfNeeded()
nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalDownloadsFragment()
)
}
HomeMenu.Item.Help -> {
hideOnboardingIfNeeded()
(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() {
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
}

@ -41,13 +41,16 @@ data class Tab(
* @property mode The state of the [HomeFragment] UI.
* @property tabs The list of opened [Tab] 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(
val collections: List<TabCollection>,
val expandedCollections: Set<Long>,
val mode: Mode,
val topSites: List<TopSite>,
val tip: Tip? = null
val tip: Tip? = null,
val showCollectionPlaceholder: Boolean
) : State
sealed class HomeFragmentAction : Action {
@ -55,7 +58,8 @@ sealed class HomeFragmentAction : Action {
val topSites: List<TopSite>,
val mode: Mode,
val collections: List<TabCollection>,
val tip: Tip? = null
val tip: Tip? = null,
val showCollectionPlaceholder: Boolean
) :
HomeFragmentAction()
@ -66,6 +70,7 @@ sealed class HomeFragmentAction : Action {
data class ModeChange(val mode: Mode) : HomeFragmentAction()
data class TopSitesChange(val topSites: List<TopSite>) : HomeFragmentAction()
data class RemoveTip(val tip: Tip) : HomeFragmentAction()
object RemoveCollectionsPlaceholder : HomeFragmentAction()
}
private fun homeFragmentStateReducer(
@ -93,6 +98,11 @@ private fun homeFragmentStateReducer(
is HomeFragmentAction.CollectionsChange -> state.copy(collections = action.collections)
is HomeFragmentAction.ModeChange -> state.copy(mode = action.mode)
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 History : Item()
object Bookmarks : Item()
object Downloads : Item()
object Quit : Item()
object Sync : Item()
}
@ -144,6 +145,14 @@ class HomeMenu(
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.
// We don't want to cause its initialization just for this check.
val accountAuthItem = if (context.components.backgroundServices.accountManagerAvailableQueue.isReady()) {
@ -158,9 +167,10 @@ class HomeMenu(
if (settings.shouldDeleteBrowsingDataOnQuit) quitItem else null,
settingsItem,
BrowserMenuDivider(),
if (FeatureFlags.syncedTabs) syncedTabsItem else null,
if (settings.syncedTabsInTabsTray) null else syncedTabsItem,
bookmarksItem,
historyItem,
if (FeatureFlags.viewDownloads) downloadsItem else null,
BrowserMenuDivider(),
addons,
BrowserMenuDivider(),

@ -8,11 +8,13 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.home.OnboardingState
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.PrivateBrowsingDescriptionViewHolder
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.OnboardingFinishViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder
@ -40,14 +42,14 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
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 {
val newTopSites = (other as? TopSiteList) ?: return false
val newTopSites = (other as? TopSitePager) ?: return false
return newTopSites.topSites == this.topSites
}
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
val newSitesSequence = newTopSites.topSites.asSequence()
val oldTopSites = this.topSites.asSequence()
@ -135,7 +137,8 @@ class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
class SessionControlAdapter(
private val interactor: SessionControlInteractor,
private val hasNormalTabsOpened: Boolean
private val viewLifecycleOwner: LifecycleOwner,
private val components: Components
) : ListAdapter<AdapterItem, RecyclerView.ViewHolder>(AdapterItemDiffCallback()) {
// 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)
return when (viewType) {
ButtonTipViewHolder.LAYOUT_ID -> ButtonTipViewHolder(view, interactor)
TopSiteViewHolder.LAYOUT_ID -> TopSiteViewHolder(view, interactor)
TopSitePagerViewHolder.LAYOUT_ID -> TopSitePagerViewHolder(view, interactor)
PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(
view,
interactor
)
NoCollectionsMessageViewHolder.LAYOUT_ID ->
NoCollectionsMessageViewHolder(view, interactor, hasNormalTabsOpened)
NoCollectionsMessageViewHolder(
view,
viewLifecycleOwner,
components.core.store,
interactor
)
CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view)
CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, interactor)
TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder(
@ -195,8 +203,8 @@ class SessionControlAdapter(
val tipItem = item as AdapterItem.TipItem
holder.bind(tipItem.tip)
}
is TopSiteViewHolder -> {
holder.bind((item as AdapterItem.TopSiteList).topSites)
is TopSitePagerViewHolder -> {
holder.bind((item as AdapterItem.TopSitePager).topSites)
}
is CollectionViewHolder -> {
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.collections.SaveCollectionStep
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.MetricController
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.HomeFragmentStore
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings
import mozilla.components.feature.tab.collections.Tab as ComponentTab
/**
@ -144,16 +144,21 @@ interface SessionControlController {
* @see [CollectionInteractor.onAddTabsToCollectionTapped]
*/
fun handleCreateCollection()
/**
* @see [CollectionInteractor.onRemoveCollectionsPlaceholder]
*/
fun handleRemoveCollectionsPlaceholder()
}
@Suppress("TooManyFunctions", "LargeClass")
class DefaultSessionControlController(
private val activity: HomeActivity,
private val settings: Settings,
private val engine: Engine,
private val metrics: MetricController,
private val sessionManager: SessionManager,
private val tabCollectionStorage: TabCollectionStorage,
private val topSiteStorage: TopSiteStorage,
private val addTabUseCase: TabsUseCases.AddNewTabUseCase,
private val fragmentStore: HomeFragmentStore,
private val navController: NavController,
@ -213,7 +218,11 @@ class DefaultSessionControlController(
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)
if (collection.tabs.size == 1) {
@ -223,7 +232,13 @@ class DefaultSessionControlController(
)
val 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 {
viewLifecycleScope.launch(Dispatchers.IO) {
tabCollectionStorage.removeTabFromCollection(collection, tab)
@ -273,7 +288,9 @@ class DefaultSessionControlController(
}
viewLifecycleScope.launch(Dispatchers.IO) {
topSiteStorage.removeTopSite(topSite)
with(activity.components.useCases.topSitesUseCase) {
removeTopSites(topSite)
}
}
}
@ -369,6 +386,11 @@ class DefaultSessionControlController(
showTabTrayCollectionCreation()
}
override fun handleRemoveCollectionsPlaceholder() {
settings.showCollectionsPlaceholderOnHome = false
fragmentStore.dispatch(HomeFragmentAction.RemoveCollectionsPlaceholder)
}
private fun showShareFragment(shareSubject: String, data: List<ShareData>) {
val directions = HomeFragmentDirections.actionGlobalShareFragment(
shareSubject = shareSubject,

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

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

@ -6,22 +6,50 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders
import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
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.utils.view.ViewHolder
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor
import org.mozilla.fenix.utils.view.ViewHolder
@OptIn(ExperimentalCoroutinesApi::class)
open class NoCollectionsMessageViewHolder(
view: View,
interactor: CollectionInteractor,
hasNormalTabsOpened: Boolean
viewLifecycleOwner: LifecycleOwner,
store: BrowserStore,
interactor: CollectionInteractor
) : ViewHolder(view) {
init {
add_tabs_to_collections_button.setOnClickListener {
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 {

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

@ -31,9 +31,9 @@ class OnboardingAutomaticSignInViewHolder(
private val headerText = view.header_text
init {
view.turn_on_sync_button.setOnClickListener {
view.fxa_sign_in_button.setOnClickListener {
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
import android.view.View
import androidx.core.content.ContextCompat
import androidx.navigation.Navigation
import androidx.recyclerview.widget.RecyclerView
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.components.metrics.Event
import org.mozilla.fenix.ext.addUnderline
import org.mozilla.fenix.ext.components
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) {
private val headerText = view.header_text
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)
val directions = HomeFragmentDirections.actionGlobalTurnOnSync()
Navigation.findNavController(view).navigate(directions)
}
view.learn_more.addUnderline()
view.learn_more.setOnClickListener {
interactor.onLearnMoreClicked()
}
}
fun bind() {
val context = itemView.context
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)
headerText.text = context.getString(R.string.onboarding_account_sign_in_header)
}
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.item.SimpleBrowserMenuItem
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.ext.components
import org.mozilla.fenix.ext.loadIntoView
@ -26,23 +28,23 @@ class TopSiteItemViewHolder(
private val interactor: TopSiteInteractor
) : ViewHolder(view) {
private lateinit var topSite: TopSite
private var topSiteMenu: TopSiteItemMenu
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 {
interactor.onSelectTopSite(topSite.url, topSite.isDefault)
interactor.onSelectTopSite(topSite.url, topSite.type === DEFAULT)
}
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)
it.setOnTouchListener @SuppressLint("ClickableViewAccessibility") { v, event ->
onTouchEvent(v, event, menu)
@ -82,6 +84,7 @@ class TopSiteItemViewHolder(
class TopSiteItemMenu(
private val context: Context,
private val isPinnedSite: Boolean,
private val onItemTapped: (Item) -> Unit = {}
) {
sealed class Item {
@ -98,9 +101,12 @@ class TopSiteItemMenu(
) {
onItemTapped.invoke(Item.OpenInPrivateTab)
},
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)
}

@ -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.navArgs
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.Main
import kotlinx.coroutines.launch
@ -31,6 +32,7 @@ import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.ktx.android.content.getColorFromAttr
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.R
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.getRootView
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.placeCursorAtEnd
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.setToolbarColors
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 androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.view.updatePaddingRelative
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
@ -86,7 +87,7 @@ class SelectBookmarkFolderAdapter(private val sharedViewModel: BookmarksSharedVi
}
val pxToIndent = dpsToIndent.dpToPx(view.context.resources.displayMetrics)
val padding = pxToIndent * if (folder.depth > maxDepth) maxDepth else folder.depth
view.setPadding(padding, 0, 0, 0)
view.updatePaddingRelative(start = padding)
}
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) {
// Enable clicking the finish button
migration_button_text_view.apply {
migration_button.apply {
setOnClickListener {
AbstractMigrationService.dismissNotification(context)
@ -78,6 +78,8 @@ class MigrationProgressActivity : AbstractMigrationProgressActivity() {
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))
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))
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)
if (description != 0) {
val spannableDescription = SpannableString(resources.getString(description))
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(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.
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))
else -> if (url.isNotBlank()) {
openSearchOrUrl(url)

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

Loading…
Cancel
Save