diff --git a/app/build.gradle b/app/build.gradle index 6778cefda..5804dc19e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,6 +11,7 @@ apply plugin: 'com.google.android.gms.oss-licenses-plugin' import com.android.build.OutputFile +import groovy.json.JsonOutput import org.gradle.internal.logging.text.StyledTextOutput.Style import org.gradle.internal.logging.text.StyledTextOutputFactory import org.mozilla.fenix.gradle.tasks.LintUnitTestRunner @@ -59,12 +60,14 @@ android { shrinkResources false minifyEnabled false applicationIdSuffix ".fenix.debug" + buildConfigField "String", "AMO_COLLECTION", "\"83a9cccfe6e24a34bd7b155ff9ee32\"" resValue "bool", "IS_DEBUG", "true" pseudoLocalesEnabled true } nightly releaseTemplate >> { applicationIdSuffix ".fenix" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" + buildConfigField "String", "AMO_COLLECTION", "\"83a9cccfe6e24a34bd7b155ff9ee32\"" def deepLinkSchemeValue = "fenix-nightly" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" manifestPlaceholders = ["deepLinkScheme": deepLinkSchemeValue] @@ -533,7 +536,7 @@ dependencies { } if (project.hasProperty("coverage")) { - tasks.withType(Test) { + tasks.withType(Test).configureEach { jacoco.includeNoLocationClasses = true } @@ -578,7 +581,7 @@ if (project.hasProperty("coverage")) { // Task for printing APK information for the requested variant // Usage: "./gradlew printVariants // ------------------------------------------------------------------------------------------------- -task printVariants { +tasks.register('printVariants') { doLast { def variants = android.applicationVariants.collect {[ apks: it.variantData.outputScope.apkDatas.collect {[ @@ -597,11 +600,13 @@ task printVariants { build_type: 'androidTest', name: 'androidTest', ]) - println 'variants: ' + groovy.json.JsonOutput.toJson(variants) + println 'variants: ' + JsonOutput.toJson(variants) } } task buildTranslationArray { + // This isn't running as a task, instead the array is build when the gradle file is parsed. + // https://github.com/mozilla-mobile/fenix/issues/14175 def foundLocales = new StringBuilder() foundLocales.append("new String[]{") @@ -618,12 +623,12 @@ task buildTranslationArray { android.defaultConfig.buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", foundLocalesString } -task lintUnitTestRunner(type: LintUnitTestRunner) +tasks.register('lintUnitTestRunner', LintUnitTestRunner) afterEvaluate { // Format test output. Ported from AC #2401 - tasks.matching {it instanceof Test}.all { + tasks.withType(Test).configureEach { systemProperty "robolectric.logging", "stdout" systemProperty "logging.test-mode", "true" @@ -711,5 +716,5 @@ tasks.register("updateCookiesExtensionVersion", Copy) { task -> updateExtensionVersion(task, 'src/main/assets/extensions/cookies') } -preBuild.dependsOn updateAdsExtensionVersion -preBuild.dependsOn updateCookiesExtensionVersion +preBuild.dependsOn "updateAdsExtensionVersion" +preBuild.dependsOn "updateCookiesExtensionVersion" diff --git a/app/metrics.yaml b/app/metrics.yaml index 202d3face..ab3a3cddb 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -3284,7 +3284,7 @@ app_theme: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-04-01" + expires: "2021-04-01" pocket: pocket_top_site_clicked: @@ -3946,3 +3946,31 @@ progressive_web_app: - fenix-core@mozilla.com - erichards@mozilla.com expires: "2021-03-01" + +master_password: + displayed: + type: event + description: | + The master password migration dialog was displayed + bugs: + - https://github.com/mozilla-mobile/fenix/pull/14468#issuecomment-684114534 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/14468#issuecomment-684114534 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2021-03-01" + migration: + type: event + description: | + Logins were successfully migrated using a master password. + bugs: + - https://github.com/mozilla-mobile/fenix/pull/14468#issuecomment-684114534 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/14468#issuecomment-684114534 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2021-03-01" diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt index 7dd20c944..4c6f93a8a 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt @@ -1,5 +1,6 @@ package org.mozilla.fenix.ui +import androidx.test.espresso.IdlingRegistry import org.mozilla.fenix.helpers.TestAssetHelper /* This Source Code Form is subject to the terms of the Mozilla Public @@ -11,8 +12,10 @@ import org.junit.Rule import org.junit.Before import org.junit.After import org.junit.Test +import org.mozilla.fenix.R import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.HomeActivityTestRule +import org.mozilla.fenix.helpers.RecyclerViewIdlingResource import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar @@ -25,6 +28,7 @@ class SettingsAddonsTest { /* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping. private lateinit var mockWebServer: MockWebServer + private var addonsListIdlingResource: RecyclerViewIdlingResource? = null @get:Rule val activityTestRule = HomeActivityTestRule() @@ -40,6 +44,10 @@ class SettingsAddonsTest { @After fun tearDown() { mockWebServer.shutdown() + + if (addonsListIdlingResource != null) { + IdlingRegistry.getInstance().unregister(addonsListIdlingResource!!) + } } // Walks through settings add-ons menu to ensure all items are present @@ -51,6 +59,9 @@ class SettingsAddonsTest { verifyAdvancedHeading() verifyAddons() }.openAddonsManagerMenu { + addonsListIdlingResource = + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.add_ons_list), 1) + IdlingRegistry.getInstance().register(addonsListIdlingResource!!) verifyAddonsItems() } } @@ -65,6 +76,9 @@ class SettingsAddonsTest { }.openNewTabAndEnterToBrowser(defaultWebPage.url) { }.openThreeDotMenu { }.openAddonsManagerMenu { + addonsListIdlingResource = + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.add_ons_list), 1) + IdlingRegistry.getInstance().register(addonsListIdlingResource!!) clickInstallAddon(addonName) verifyAddonPrompt(addonName) cancelInstallAddon() @@ -85,6 +99,9 @@ class SettingsAddonsTest { verifyAdvancedHeading() verifyAddons() }.openAddonsManagerMenu { + addonsListIdlingResource = + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.add_ons_list), 1) + IdlingRegistry.getInstance().register(addonsListIdlingResource!!) clickInstallAddon(addonName) acceptInstallAddon() verifyDownloadAddonPrompt(addonName, activityTestRule) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt index c0a87fe13..381cc9862 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt @@ -52,6 +52,8 @@ class SettingsAdvancedTest { // ADVANCED verifyAdvancedHeading() verifyAddons() + verifyOpenLinksInAppsButton() + verifyOpenLinksInAppsSwitchDefault() verifyRemoteDebug() verifyLeakCanaryButton() } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt index 4e1127875..5e5c69a1f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt @@ -69,6 +69,7 @@ class SettingsBasicsTest { verifyBasicsHeading() verifySearchEngineButton() verifyDefaultBrowserItem() + verifyCloseTabsItem() // drill down to submenu }.openSearchSubMenu { verifyDefaultSearchEngineHeader() @@ -168,6 +169,17 @@ class SettingsBasicsTest { } } + @Test + fun changeCloseTabsSetting() { + // Goes through the settings and verified the close tabs setting options. + homeScreen { + }.openThreeDotMenu { + }.openSettings { + }.openCloseTabsSubMenu { + verifyOptions() + } + } + @Test fun changeAccessibiltySettings() { // Goes through the settings and changes the default text on a webpage, then verifies if the text has changed. diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt index a963f5c51..a61808773 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt @@ -147,10 +147,6 @@ class SettingsPrivacyTest { verifyNavigationToolBarHeader() verifyDataCollectionSubMenuItems() }.goBack { - - // OPEN LINKS IN APPS - verifyOpenLinksInAppsButton() - verifyOpenLinksInAppsSwitchDefault() }.goBack { verifyHomeComponent() } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt index 086d08186..695fac0d0 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt @@ -285,14 +285,6 @@ class SmokeTest { }.goToSearchEngine { }.enterURLAndEnterToBrowser(defaultWebPage.url) { }.openTabDrawer { - }.openNewTab { - clickSearchEngineButton() - mDevice.waitForIdle() - changeDefaultSearchEngine("Twitter") - verifySearchEngineIcon("Twitter") - }.goToSearchEngine { - }.enterURLAndEnterToBrowser(defaultWebPage.url) { - }.openTabDrawer { }.openNewTab { clickSearchEngineButton() changeDefaultSearchEngine("Wikipedia") diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt index 942848e76..f25e14492 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt @@ -248,8 +248,6 @@ private fun assertSearchEngineList() { .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))) } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt index 073148cde..5ed56bb1e 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt @@ -51,6 +51,7 @@ class SettingsRobot { fun verifyAccessibilityButton() = assertAccessibilityButton() fun verifySetAsDefaultBrowserButton() = assertSetAsDefaultBrowserButton() fun verifyDefaultBrowserItem() = assertDefaultBrowserItem() + fun verifyCloseTabsItem() = assertCloseTabsItem() fun verifyDefaultBrowserIsDisaled() = assertDefaultBrowserIsDisabled() fun clickDefaultBrowserSwitch() = toggleDefaultBrowserSwitch() fun verifyAndroidDefaultAppsMenuAppears() = assertAndroidDefaultAppsMenuAppears() @@ -134,6 +135,15 @@ class SettingsRobot { return SettingsSubMenuThemeRobot.Transition() } + fun openCloseTabsSubMenu(interact: SettingsSubMenuTabsRobot.() -> Unit): SettingsSubMenuTabsRobot.Transition { + + fun closeTabsButton() = onView(withText("Close tabs")) + closeTabsButton().click() + + SettingsSubMenuTabsRobot().interact() + return SettingsSubMenuTabsRobot.Transition() + } + fun openAccessibilitySubMenu(interact: SettingsSubMenuAccessibilityRobot.() -> Unit): SettingsSubMenuAccessibilityRobot.Transition { fun accessibilityButton() = onView(withText("Accessibility")) @@ -237,8 +247,11 @@ private fun assertSettingsView() { } // GENERAL SECTION -private fun assertGeneralHeading() = onView(withText("General")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +private fun assertGeneralHeading() { + scrollToElementByText("General") + onView(withText("General")) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +} private fun assertSearchEngineButton() { mDevice.wait(Until.findObject(By.text("Search")), waitingTime) @@ -284,8 +297,15 @@ private fun assertDefaultBrowserItem() { .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } +private fun assertCloseTabsItem() { + mDevice.wait(Until.findObject(By.text("Close tabs")), waitingTime) + onView(withText("Close tabs")) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +} + // PRIVACY SECTION private fun assertPrivacyHeading() { + scrollToElementByText("Privacy and security") onView(withText("Privacy and security")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } @@ -345,11 +365,16 @@ private fun assertDataCollectionButton() = onView(withText("Data collection")) private fun openLinksInAppsButton() = onView(withText("Open links in apps")) -private fun assertOpenLinksInAppsButton() = openLinksInAppsButton() - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +private fun assertOpenLinksInAppsButton() { + scrollToElementByText("Open links in apps") + openLinksInAppsButton() + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +} -private fun assertOpenLinksInAppsValue() = openLinksInAppsButton() - .assertIsEnabled(isEnabled = true) +private fun assertOpenLinksInAppsValue() { + scrollToElementByText("Open links in apps") + openLinksInAppsButton().assertIsEnabled(isEnabled = true) +} // DEVELOPER TOOLS SECTION private fun assertDeveloperToolsHeading() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt index 55c1dc3d6..13f9cd795 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt @@ -64,8 +64,6 @@ private fun assertSearchEngineList() { .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))) onView(withText("Add search engine")) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuTabsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuTabsRobot.kt new file mode 100644 index 000000000..d1fbaf01b --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuTabsRobot.kt @@ -0,0 +1,58 @@ +/* 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/. */ + +@file:Suppress("TooManyFunctions") + +package org.mozilla.fenix.ui.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import org.hamcrest.CoreMatchers.allOf + +/** + * Implementation of Robot Pattern for the settings Tabs sub menu. + */ +class SettingsSubMenuTabsRobot { + + fun verifyOptions() = assertOptions() + + class Transition { + val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { + mDevice.waitForIdle() + goBackButton().perform(ViewActions.click()) + + SettingsRobot().interact() + return SettingsRobot.Transition() + } + } +} + +private fun assertOptions() { + afterOneDayToggle() + .check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + manualToggle() + .check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + afterOneWeekToggle() + .check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + afterOneMonthToggle() + .check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) +} + +private fun manualToggle() = onView(withText("Manually")) + +private fun afterOneDayToggle() = onView(withText("After one day")) + +private fun afterOneWeekToggle() = onView(withText("After one week")) + +private fun afterOneMonthToggle() = onView(withText("After one month")) + +private fun goBackButton() = + onView(allOf(ViewMatchers.withContentDescription("Navigate up"))) diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index 69b434d02..1db3b7c2c 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -21,10 +21,6 @@ object FeatureFlags { */ val syncedTabsInTabsTray = Config.channel.isNightlyOrDebug - /** - * Enables viewing tab history - */ - val tabHistory = true /** * Enables the new search experience */ @@ -38,19 +34,11 @@ object FeatureFlags { /** * Enables wait til first contentful paint */ - val waitUntilPaintToDraw = true // Just enables the setting in Secret Settings + val waitUntilPaintToDraw = true /** * Enables downloads with external download managers. */ - val externalDownloadManager = true // Just enables the setting in Secret Settings + val externalDownloadManager = true - /** - * Enables viewing downloads in browser. - */ - val viewDownloads = true - /** - * Enables selecting from multiple logins. - */ - val loginSelect = true } diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index c1e6500ac..e3b7e9ed6 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -164,9 +164,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { } private fun restoreDownloads() { - if (FeatureFlags.viewDownloads) { - components.useCases.downloadUseCases.restoreDownloads() - } + components.useCases.downloadUseCases.restoreDownloads() } private fun initVisualCompletenessQueueAndQueueTasks() { diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index bc49ff745..a40e9afde 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -281,6 +281,14 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { } settings().wasDefaultBrowserOnLastResume = settings().isDefaultBrowser() + + if (!settings().manuallyCloseTabs) { + components.core.store.state.tabs.filter { + (System.currentTimeMillis() - it.lastAccess) > settings().getTabTimeout() + }.forEach { + components.useCases.tabsUseCases.removeTab(it.id) + } + } } } @@ -462,8 +470,14 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { super.onBackPressed() } - private fun isAndroidN(): Boolean = - Build.VERSION.SDK_INT == Build.VERSION_CODES.N || Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1 + private fun shouldUseCustomBackLongPress(): Boolean { + val isAndroidN = + Build.VERSION.SDK_INT == Build.VERSION_CODES.N || Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1 + // Huawei devices seem to have problems with onKeyLongPress + // See https://github.com/mozilla-mobile/fenix/issues/13498 + val isHuawei = Build.MANUFACTURER.equals("huawei", ignoreCase = true) + return isAndroidN || isHuawei + } private fun handleBackLongPress(): Boolean { supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach { @@ -476,12 +490,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { final override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { // Inspired by https://searchfox.org/mozilla-esr68/source/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java#584-613 - // Android N has broken passing onKeyLongPress events for the back button, so we + // Android N and Huawei devices have broken onKeyLongPress events for the back button, so we // instead implement the long press behavior ourselves // - For short presses, we cancel the callback in onKeyUp // - For long presses, the normal keypress is marked as cancelled, hence won't be handled elsewhere // (but Android still provides the haptic feedback), and the long press action is run - if (isAndroidN() && keyCode == KeyEvent.KEYCODE_BACK) { + if (shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) { backLongPressJob = lifecycleScope.launch { delay(ViewConfiguration.getLongPressTimeout().toLong()) handleBackLongPress() @@ -491,7 +505,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { } final override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { - if (isAndroidN() && keyCode == KeyEvent.KEYCODE_BACK) { + if (shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) { backLongPressJob?.cancel() } return super.onKeyUp(keyCode, event) @@ -500,7 +514,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { final override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean { // onKeyLongPress is broken in Android N so we don't handle back button long presses here // for N. The version check ensures we don't handle back button long presses twice. - if (!isAndroidN() && keyCode == KeyEvent.KEYCODE_BACK) { + if (!shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) { return handleBackLongPress() } return super.onKeyLongPress(keyCode, event) diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index d8620a3cc..8e02c2b36 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -508,7 +508,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session onNeedToRequestPermissions = { permissions -> requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS) }, - loginPickerView = if (FeatureFlags.loginSelect) loginSelectBar else null, + loginPickerView = loginSelectBar, onManageLogins = { browserAnimator.captureEngineViewAndDrawStatically { val directions = @@ -1163,6 +1163,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session } override fun onAccessibilityStateChanged(enabled: Boolean) { - browserToolbarView.setScrollFlags(enabled) + if (_browserToolbarView != null) { + browserToolbarView.setScrollFlags(enabled) + } } } diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index 3b0a5bdb8..ec21fee58 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -111,7 +111,7 @@ class Core(private val context: Context, private val crashReporter: CrashReporti * This is consistent with both Fennec and Firefox Desktop. */ if (Config.channel.isNightlyOrDebug || Config.channel.isBeta) { - WebCompatReporterFeature.install(it) + WebCompatReporterFeature.install(it, "fenix") } } } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt index 1ca03a57c..db78aa466 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt @@ -186,6 +186,9 @@ sealed class Event { object ProgressiveWebAppOpenFromHomescreenTap : Event() object ProgressiveWebAppInstallAsShortcut : Event() + object MasterPasswordMigrationSuccess : Event() + object MasterPasswordMigrationDisplayed : Event() + // Interaction events with extras data class ProgressiveWebAppForeground(val timeForegrounded: Long) : Event() { diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt index a89499fce..219a7fd19 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt @@ -27,6 +27,7 @@ import org.mozilla.fenix.GleanMetrics.FindInPage import org.mozilla.fenix.GleanMetrics.History import org.mozilla.fenix.GleanMetrics.LoginDialog import org.mozilla.fenix.GleanMetrics.Logins +import org.mozilla.fenix.GleanMetrics.MasterPassword import org.mozilla.fenix.GleanMetrics.MediaNotification import org.mozilla.fenix.GleanMetrics.MediaState import org.mozilla.fenix.GleanMetrics.Metrics @@ -684,6 +685,13 @@ private val Event.wrapper: EventWrapper<*>? { ProgressiveWebApp.backgroundKeys.valueOf(it) } ) + Event.MasterPasswordMigrationDisplayed -> EventWrapper( + { MasterPassword.displayed.record(it) } + ) + Event.MasterPasswordMigrationSuccess -> EventWrapper( + { MasterPassword.migration.record(it) } + ) + // Don't record other events in Glean: is Event.AddBookmark -> null is Event.OpenedBookmark -> null diff --git a/app/src/main/java/org/mozilla/fenix/components/tips/TipManager.kt b/app/src/main/java/org/mozilla/fenix/components/tips/TipManager.kt index 1cb82131f..9a58953d7 100644 --- a/app/src/main/java/org/mozilla/fenix/components/tips/TipManager.kt +++ b/app/src/main/java/org/mozilla/fenix/components/tips/TipManager.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix.components.tips +import android.graphics.drawable.Drawable + sealed class TipType { data class Button(val text: String, val action: () -> Unit) : TipType() } @@ -13,7 +15,8 @@ open class Tip( val identifier: String, val title: String, val description: String, - val learnMoreURL: String? + val learnMoreURL: String?, + val titleDrawable: Drawable? = null ) interface TipProvider { diff --git a/app/src/main/java/org/mozilla/fenix/components/tips/providers/MasterPasswordTipProvider.kt b/app/src/main/java/org/mozilla/fenix/components/tips/providers/MasterPasswordTipProvider.kt new file mode 100644 index 000000000..e578f90fa --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/tips/providers/MasterPasswordTipProvider.kt @@ -0,0 +1,265 @@ +/* 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.tips.providers + +import android.content.Context +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import com.google.android.material.button.MaterialButton +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import io.sentry.Sentry +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mozilla.appservices.logins.IdCollisionException +import mozilla.appservices.logins.InvalidRecordException +import mozilla.appservices.logins.LoginsStorageException +import mozilla.appservices.logins.ServerPassword +import mozilla.components.concept.storage.Login +import mozilla.components.support.migration.FennecLoginsMPImporter +import mozilla.components.support.migration.FennecProfile +import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.tips.Tip +import org.mozilla.fenix.components.tips.TipProvider +import org.mozilla.fenix.components.tips.TipType +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.metrics +import org.mozilla.fenix.ext.settings + +/** + * Tip explaining to master password users how to migrate their logins. + */ +class MasterPasswordTipProvider( + private val context: Context, + private val navigateToLogins: () -> Unit, + private val dismissTip: (Tip) -> Unit +) : TipProvider { + + private val fennecLoginsMPImporter: FennecLoginsMPImporter? by lazy { + FennecProfile.findDefault( + context, + context.components.analytics.crashReporter + )?.let { + FennecLoginsMPImporter( + it + ) + } + } + + override val tip: Tip? by lazy { masterPasswordMigrationTip() } + + override val shouldDisplay: Boolean by lazy { + context.settings().shouldDisplayMasterPasswordMigrationTip && + fennecLoginsMPImporter?.hasMasterPassword() == true + } + + private fun masterPasswordMigrationTip(): Tip = + Tip( + type = TipType.Button( + text = context.getString(R.string.mp_homescreen_button), + action = ::showMasterPasswordMigration + ), + identifier = context.getString(R.string.pref_key_master_password_tip), + title = context.getString(R.string.mp_homescreen_tip_title), + description = context.getString(R.string.mp_homescreen_tip_message), + learnMoreURL = null, + titleDrawable = ContextCompat.getDrawable(context, R.drawable.ic_login) + ) + + private fun showMasterPasswordMigration() { + val dialogView = LayoutInflater.from(context).inflate(R.layout.mp_migration_dialog, null) + + val dialogBuilder = AlertDialog.Builder(context).apply { + setTitle(context.getString(R.string.mp_dialog_title_recovery_transfer_saved_logins)) + setMessage(context.getString(R.string.mp_dialog_message_recovery_transfer_saved_logins)) + setView(dialogView) + create() + } + + val dialog = dialogBuilder.show() + + context.metrics.track(Event.MasterPasswordMigrationDisplayed) + + val passwordErrorText = context.getString(R.string.mp_dialog_error_transfer_saved_logins) + val migrationContinueButton = + dialogView.findViewById(R.id.migration_continue) + val passwordView = dialogView.findViewById(R.id.password_field) + val passwordLayout = + dialogView.findViewById(R.id.password_text_input_layout) + passwordView.addTextChangedListener( + object : TextWatcher { + var isValid = false + override fun afterTextChanged(p: Editable?) { + when { + p.toString().isEmpty() -> { + isValid = false + passwordLayout.error = passwordErrorText + } + else -> { + val possiblePassword = passwordView.text.toString() + isValid = + fennecLoginsMPImporter?.checkPassword(possiblePassword) == true + passwordLayout.error = if (isValid) null else passwordErrorText + } + } + migrationContinueButton.alpha = if (isValid) 1F else HALF_OPACITY + migrationContinueButton.isEnabled = isValid + } + + override fun beforeTextChanged( + p: CharSequence?, + start: Int, + count: Int, + after: Int + ) { + // NOOP + } + + override fun onTextChanged(p: CharSequence?, start: Int, before: Int, count: Int) { + // NOOP + } + }) + + migrationContinueButton.apply { + setOnClickListener { + // Step 1: Verify the password again before trying to use it + val possiblePassword = passwordView.text.toString() + val isValid = fennecLoginsMPImporter?.checkPassword(possiblePassword) == true + + // Step 2: With valid MP, get logins and complete the migration + if (isValid) { + val logins = fennecLoginsMPImporter?.getLoginRecords( + possiblePassword, + context.components.analytics.crashReporter + ) + + if (logins.isNullOrEmpty()) { + showFailureDialog() + dialog.dismiss() + } else { + saveLogins(logins, dialog) + } + } else { + passwordView.error = + context?.getString(R.string.mp_dialog_error_transfer_saved_logins) + } + } + } + + dialogView.findViewById(R.id.migration_cancel).apply { + setOnClickListener { + dialog.dismiss() + } + } + } + + private fun showFailureDialog() { + val dialogView = + LayoutInflater.from(context).inflate(R.layout.mp_migration_done_dialog, null) + + val dialogBuilder = AlertDialog.Builder(context).apply { + setTitle(context.getString(R.string.mp_dialog_title_transfer_failure)) + setMessage(context.getString(R.string.mp_dialog_message_transfer_failure)) + setView(dialogView) + create() + } + + val dialog = dialogBuilder.show() + + dialogView.findViewById(R.id.positive_button).apply { + text = context.getString(R.string.mp_dialog_close_transfer) + setOnClickListener { + tip?.let { dismissTip(it) } + dialog.dismiss() + } + } + dialogView.findViewById(R.id.negative_button).apply { + isVisible = false + } + } + + private fun saveLogins(logins: List, dialog: AlertDialog) { + CoroutineScope(IO).launch { + logins.map { it.toLogin() }.forEach { + try { + context.components.core.passwordsStorage.add(it) + } catch (e: InvalidRecordException) { + // This record was invalid and we couldn't save this login + Sentry.capture("Master Password migration add login error $e for reason ${e.reason}") + } catch (e: IdCollisionException) { + // Nonempty ID was provided + Sentry.capture("Master Password migration add login error $e") + } catch (e: LoginsStorageException) { + // Some other error occurred + Sentry.capture("Master Password migration add login error $e") + } + } + withContext(Dispatchers.Main) { + // Step 3: Dismiss this dialog and show the success dialog + showSuccessDialog() + dialog.dismiss() + } + } + } + + private fun showSuccessDialog() { + tip?.let { dismissTip(it) } + + context.metrics.track(Event.MasterPasswordMigrationSuccess) + + val dialogView = + LayoutInflater.from(context).inflate(R.layout.mp_migration_done_dialog, null) + + val dialogBuilder = AlertDialog.Builder(context).apply { + setTitle(context.getString(R.string.mp_dialog_title_transfer_success)) + setMessage(context.getString(R.string.mp_dialog_message_transfer_success)) + setView(dialogView) + create() + } + + val dialog = dialogBuilder.show() + + dialogView.findViewById(R.id.positive_button).apply { + setOnClickListener { + navigateToLogins() + dialog.dismiss() + } + } + dialogView.findViewById(R.id.negative_button).apply { + setOnClickListener { + dialog.dismiss() + } + } + } + + /** + * Converts an Application Services [ServerPassword] to an Android Components [Login] + */ + fun ServerPassword.toLogin() = Login( + origin = hostname, + formActionOrigin = formSubmitURL, + httpRealm = httpRealm, + username = username, + password = password, + timesUsed = timesUsed, + timeCreated = timeCreated, + timeLastUsed = timeLastUsed, + timePasswordChanged = timePasswordChanged, + usernameField = usernameField, + passwordField = passwordField + ) + + companion object { + private const val HALF_OPACITY = .5F + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt index ff549ffa2..bf41b9613 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt @@ -24,7 +24,6 @@ import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.storage.BookmarksStorage import mozilla.components.support.ktx.android.content.getColorFromAttr -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode @@ -181,7 +180,7 @@ class DefaultToolbarMenu( .syncedTabsInTabsTray val menuItems = listOfNotNull( - if (FeatureFlags.viewDownloads) downloadsItem else null, + downloadsItem, historyItem, bookmarksItem, if (syncedTabsInTabsTray) null else syncedTabs, diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index d9eb1fecb..26db79972 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -82,6 +82,8 @@ import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.tips.FenixTipManager +import org.mozilla.fenix.components.tips.Tip +import org.mozilla.fenix.components.tips.providers.MasterPasswordTipProvider import org.mozilla.fenix.components.tips.providers.MigrationTipProvider import org.mozilla.fenix.components.toolbar.TabCounterMenu import org.mozilla.fenix.components.toolbar.ToolbarPosition @@ -174,6 +176,7 @@ class HomeFragment : Fragment() { } } + @Suppress("LongMethod") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -197,7 +200,18 @@ class HomeFragment : Fragment() { expandedCollections = emptySet(), mode = currentMode.getCurrentMode(), topSites = components.core.topSiteStorage.cachedTopSites, - tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip(), + tip = StrictMode.allowThreadDiskReads().resetPoliciesAfter { + FenixTipManager( + listOf( + MasterPasswordTipProvider( + requireContext(), + ::navToSavedLogins, + ::dismissTip + ), + MigrationTipProvider(requireContext()) + ) + ).getTip() + }, showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome ) ) @@ -232,6 +246,7 @@ class HomeFragment : Fragment() { handleSwipedItemDeletionCancel = ::handleSwipedItemDeletionCancel ) ) + updateLayout(view) sessionControlView = SessionControlView( view.sessionControlRecyclerView, @@ -246,6 +261,10 @@ class HomeFragment : Fragment() { return view } + private fun dismissTip(tip: Tip) { + sessionControlInteractor.onCloseTip(tip) + } + /** * Returns a [TopSitesConfig] which specifies how many top sites to display and whether or * not frequently visited sites should be displayed. @@ -411,7 +430,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 { - val willNavigateToSearch = !bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience + val willNavigateToSearch = + !bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience if (!browsingModeManager.mode.isPrivate && !willNavigateToSearch) { SearchWidgetCFR( context = view.context, @@ -540,7 +560,18 @@ class HomeFragment : Fragment() { collections = components.core.tabCollectionStorage.cachedTabCollections, mode = currentMode.getCurrentMode(), topSites = components.core.topSiteStorage.cachedTopSites, - tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip(), + tip = StrictMode.allowThreadDiskReads().resetPoliciesAfter { + FenixTipManager( + listOf( + MasterPasswordTipProvider( + requireContext(), + ::navToSavedLogins, + ::dismissTip + ), + MigrationTipProvider(requireContext()) + ) + ).getTip() + }, showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome ) ) @@ -587,6 +618,10 @@ class HomeFragment : Fragment() { } } + private fun navToSavedLogins() { + findNavController().navigate(HomeFragmentDirections.actionGlobalSavedLoginsAuthFragment()) + } + private fun dispatchModeChanges(mode: Mode) { if (mode != Mode.fromBrowsingMode(browsingModeManager.mode)) { homeFragmentStore.dispatch(HomeFragmentAction.ModeChange(mode)) diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt b/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt index 579e4bf95..f9cf7ed2b 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt @@ -21,7 +21,6 @@ import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount import mozilla.components.support.ktx.android.content.getColorFromAttr -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings @@ -170,7 +169,7 @@ class HomeMenu( if (settings.syncedTabsInTabsTray) null else syncedTabsItem, bookmarksItem, historyItem, - if (FeatureFlags.viewDownloads) downloadsItem else null, + downloadsItem, BrowserMenuDivider(), addons, BrowserMenuDivider(), diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingAutomaticSignInViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingAutomaticSignInViewHolder.kt index c09fb31d2..338b514b9 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingAutomaticSignInViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingAutomaticSignInViewHolder.kt @@ -41,7 +41,7 @@ class OnboardingAutomaticSignInViewHolder( fun bind(account: ShareableAccount) { shareableAccount = account headerText.text = itemView.context.getString( - R.string.onboarding_firefox_account_auto_signin_header_2, account.email + R.string.onboarding_firefox_account_auto_signin_header_3, account.email ) val icon = getDrawable(itemView.context, R.drawable.ic_onboarding_avatar_anonymous) headerText.putCompoundDrawablesRelativeWithIntrinsicBounds(start = icon) diff --git a/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt index 17b31b35b..9a9bdeb53 100644 --- a/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt @@ -37,6 +37,9 @@ class ButtonTipViewHolder( metrics.track(Event.TipDisplayed(tip.identifier)) tip_header_text.text = tip.title + tip.titleDrawable?.let { + tip_header_text.setCompoundDrawablesWithIntrinsicBounds(it, null, null, null) + } tip_description_text.text = tip.description tip_button.text = tip.type.text diff --git a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt index c67ed7b96..40e2d49bb 100644 --- a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt @@ -17,6 +17,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.ViewStub +import android.view.WindowManager import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatDialogFragment import androidx.constraintlayout.widget.ConstraintProperties.BOTTOM @@ -40,6 +41,7 @@ import mozilla.components.feature.qr.QrFeature import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.hasCamera import mozilla.components.support.ktx.android.content.res.getSpanned import mozilla.components.support.ktx.android.view.hideKeyboard @@ -79,6 +81,21 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { private val qrFeature = ViewBoundFeatureWrapper() private val speechIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) + override fun onStart() { + super.onStart() + // https://github.com/mozilla-mobile/fenix/issues/14279 + // To prevent GeckoView from resizing we're going to change the softInputMode to not adjust + // the size of the window. + requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) + } + + override fun onStop() { + super.onStop() + // https://github.com/mozilla-mobile/fenix/issues/14279 + // Let's reset back to the default behavior after we're done searching + requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NO_TITLE, R.style.SearchDialogStyle) @@ -254,6 +271,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { updateSearchSuggestionsHintVisibility(it) updateClipboardSuggestion(it, requireContext().components.clipboardHandler.url) updateToolbarContentDescription(it) + updateSearchShortcutsIcon(it) toolbarView.update(it) awesomeBarView.update(it) firstUpdate = false @@ -416,6 +434,20 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { urlView?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO } + private fun updateSearchShortcutsIcon(searchState: SearchFragmentState) { + view?.apply { + search_engines_shortcut_button.isVisible = searchState.areShortcutsAvailable + + val showShortcuts = searchState.showSearchShortcuts + search_engines_shortcut_button.isChecked = showShortcuts + + val color = if (showShortcuts) R.attr.contrastText else R.attr.primaryText + search_engines_shortcut_button.compoundDrawables[0]?.setTint( + requireContext().getColorFromAttr(color) + ) + } + } + companion object { private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1 } diff --git a/app/src/main/java/org/mozilla/fenix/settings/CloseTabsSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/CloseTabsSettingsFragment.kt new file mode 100644 index 000000000..9bb15f295 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/CloseTabsSettingsFragment.kt @@ -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.settings + +import android.os.Bundle +import androidx.preference.PreferenceFragmentCompat +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.utils.view.addToRadioGroup + +/** + * Lets the user customize auto closing tabs. + */ +class CloseTabsSettingsFragment : PreferenceFragmentCompat() { + private lateinit var radioManual: RadioButtonPreference + private lateinit var radioOneDay: RadioButtonPreference + private lateinit var radioOneWeek: RadioButtonPreference + private lateinit var radioOneMonth: RadioButtonPreference + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.close_tabs_preferences, rootKey) + } + + override fun onResume() { + super.onResume() + showToolbar(getString(R.string.preferences_close_tabs)) + setupPreferences() + } + + private fun setupPreferences() { + radioManual = requirePreference(R.string.pref_key_close_tabs_manually) + radioOneDay = requirePreference(R.string.pref_key_close_tabs_after_one_day) + radioOneWeek = requirePreference(R.string.pref_key_close_tabs_after_one_week) + radioOneMonth = requirePreference(R.string.pref_key_close_tabs_after_one_month) + setupRadioGroups() + } + + private fun setupRadioGroups() { + addToRadioGroup( + radioManual, + radioOneDay, + radioOneMonth, + radioOneWeek + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt index 86c117bd2..3bebc0fa2 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -170,6 +170,10 @@ class SettingsFragment : PreferenceFragmentCompat() { } } + val tabSettingsPreference = + requirePreference(R.string.pref_key_close_tabs) + tabSettingsPreference.summary = context?.settings()?.getTabTimeoutString() + setupPreferences() if (shouldUpdateAccountUIState) { @@ -192,6 +196,9 @@ class SettingsFragment : PreferenceFragmentCompat() { resources.getString(R.string.pref_key_sign_in) -> { SettingsFragmentDirections.actionSettingsFragmentToTurnOnSyncFragment() } + resources.getString(R.string.pref_key_close_tabs) -> { + SettingsFragmentDirections.actionSettingsFragmentToCloseTabsSettingsFragment() + } resources.getString(R.string.pref_key_search_settings) -> { SettingsFragmentDirections.actionSettingsFragmentToSearchEngineFragment() } @@ -301,7 +308,8 @@ class SettingsFragment : PreferenceFragmentCompat() { requirePreference(R.string.pref_key_external_download_manager) val preferenceLeakCanary = findPreference(leakKey) val preferenceRemoteDebugging = findPreference(debuggingKey) - val preferenceMakeDefaultBrowser = requirePreference(R.string.pref_key_make_default_browser) + val preferenceMakeDefaultBrowser = + requirePreference(R.string.pref_key_make_default_browser) if (!Config.channel.isReleased) { preferenceLeakCanary?.setOnPreferenceChangeListener { _, newValue -> diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/controller/SavedLoginsStorageController.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/controller/SavedLoginsStorageController.kt index 754ddb688..bd1bbe96a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/controller/SavedLoginsStorageController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/controller/SavedLoginsStorageController.kt @@ -124,7 +124,6 @@ open class SavedLoginsStorageController( fun findPotentialDuplicates(loginId: String) { var deferredLogin: Deferred>? = null - // What scope should be used here? val fetchLoginJob = viewLifecycleScope.launch(ioDispatcher) { deferredLogin = async { val login = getLogin(loginId) diff --git a/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineListPreference.kt b/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineListPreference.kt index e06f12571..1d4468a5a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineListPreference.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineListPreference.kt @@ -76,6 +76,11 @@ abstract class SearchEngineListPreference @JvmOverloads constructor( it.identifier == defaultEngine } ?: searchEngineList.list.first()).identifier + context.components.search.searchEngineManager.defaultSearchEngine = + searchEngineList.list.find { + it.identifier == selectedEngine + } + searchEngineGroup!!.removeAllViews() val layoutInflater = LayoutInflater.from(context) @@ -97,6 +102,10 @@ abstract class SearchEngineListPreference @JvmOverloads constructor( engineItem.tag = engineId if (engineId == selectedEngine) { updateDefaultItem(engineItem.radio_button) + /* #11465 -> radio_button.isChecked = true does not trigger + * onSearchEngineSelected because searchEngineGroup has null views at that point. + * So we trigger it here.*/ + onSearchEngineSelected(engine) } searchEngineGroup!!.addView(engineItem, layoutParams) } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt index 7a3a03eee..c3b4df91a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt @@ -31,6 +31,7 @@ import org.mozilla.fenix.home.HomeFragment interface TabTrayController { fun onNewTabTapped(private: Boolean) fun onTabTrayDismissed() + fun handleTabSettingsClicked() fun onShareTabsClicked(private: Boolean) fun onSyncedTabClicked(syncTab: SyncTab) fun onSaveToCollectionClicked(selectedTabs: Set) @@ -88,6 +89,10 @@ class DefaultTabTrayController( ) } + override fun handleTabSettingsClicked() { + navController.navigate(TabTrayDialogFragmentDirections.actionGlobalCloseTabSettingsFragment()) + } + override fun onTabTrayDismissed() { dismissTabTray() } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt index b6a65dd77..2ec18d928 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt @@ -24,6 +24,11 @@ interface TabTrayInteractor { */ fun onShareTabsClicked(private: Boolean) + /** + * Called when user clicks the tab settings button. + */ + fun onTabSettingsClicked() + /** * Called when user clicks button to save selected tabs to a collection. */ @@ -83,6 +88,10 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab controller.onTabTrayDismissed() } + override fun onTabSettingsClicked() { + controller.handleTabSettingsClicked() + } + override fun onShareTabsClicked(private: Boolean) { controller.onShareTabsClicked(private) } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt index e88b21d9e..36aeae234 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt @@ -198,6 +198,7 @@ class TabTrayView( is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsClicked( isPrivateModeSelected ) + is TabTrayItemMenu.Item.OpenTabSettings -> interactor.onTabSettingsClicked() is TabTrayItemMenu.Item.SaveToCollection -> interactor.onEnterMultiselect() is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked( isPrivateModeSelected @@ -554,6 +555,7 @@ class TabTrayItemMenu( sealed class Item { object ShareAllTabs : Item() + object OpenTabSettings : Item() object SaveToCollection : Item() object CloseAllTabs : Item() } @@ -578,6 +580,13 @@ class TabTrayItemMenu( onItemTapped.invoke(Item.ShareAllTabs) }, + SimpleBrowserMenuItem( + context.getString(R.string.tab_tray_menu_tab_settings), + textColorResource = R.color.primary_text_normal_theme + ) { + onItemTapped.invoke(Item.OpenTabSettings) + }, + SimpleBrowserMenuItem( context.getString(R.string.tab_tray_menu_item_close), textColorResource = R.color.primary_text_normal_theme diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt index eb1526a85..32e49ff4e 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt @@ -95,10 +95,7 @@ class TabTrayViewHolder( contentDescription = context.getString(R.string.mozac_feature_media_notification_action_play) setImageDrawable( - AppCompatResources.getDrawable( - context, - R.drawable.tab_tray_play_with_background - ) + AppCompatResources.getDrawable(context, R.drawable.media_state_play) ) } @@ -107,10 +104,7 @@ class TabTrayViewHolder( contentDescription = context.getString(R.string.mozac_feature_media_notification_action_pause) setImageDrawable( - AppCompatResources.getDrawable( - context, - R.drawable.tab_tray_pause_with_background - ) + AppCompatResources.getDrawable(context, R.drawable.media_state_pause) ) } diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index 731db9f72..9cef53a4a 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -62,6 +62,10 @@ class Settings(private val appContext: Context) : PreferencesHolder { private const val CFR_COUNT_CONDITION_FOCUS_NOT_INSTALLED = 3 private const val MIN_DAYS_SINCE_FEEDBACK_PROMPT = 120 + const val ONE_DAY_MS = 60 * 60 * 24 * 1000L + const val ONE_WEEK_MS = 60 * 60 * 24 * 7 * 1000L + const val ONE_MONTH_MS = (60 * 60 * 24 * 365 * 1000L) / 12 + private fun Action.toInt() = when (this) { Action.BLOCKED -> BLOCKED_INT Action.ASK_TO_ALLOW -> ASK_TO_ALLOW_INT @@ -159,6 +163,11 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = false ) + var shouldDisplayMasterPasswordMigrationTip by booleanPreference( + appContext.getString(R.string.pref_key_master_password_tip), + true + ) + // If any of the prefs have been modified, quit displaying the fenix moved tip fun shouldDisplayFenixMovingTip(): Boolean = preferences.getBoolean( @@ -324,6 +333,48 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = false ) + var manuallyCloseTabs by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_close_tabs_manually), + default = true + ) + + var closeTabsAfterOneDay by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_close_tabs_after_one_day), + default = false + ) + + var closeTabsAfterOneWeek by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_close_tabs_after_one_week), + default = false + ) + + var closeTabsAfterOneMonth by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_close_tabs_after_one_month), + default = false + ) + + fun getTabTimeout(): Long = when { + closeTabsAfterOneDay -> ONE_DAY_MS + closeTabsAfterOneWeek -> ONE_WEEK_MS + closeTabsAfterOneMonth -> ONE_MONTH_MS + else -> System.currentTimeMillis() + } + + fun getTabTimeoutString(): String = when { + closeTabsAfterOneDay -> { + appContext.getString(R.string.close_tabs_after_one_day) + } + closeTabsAfterOneWeek -> { + appContext.getString(R.string.close_tabs_after_one_week) + } + closeTabsAfterOneMonth -> { + appContext.getString(R.string.close_tabs_after_one_month) + } + else -> { + appContext.getString(R.string.close_tabs_manually) + } + } + val shouldUseDarkTheme by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_dark_theme), default = false diff --git a/app/src/main/res/drawable/ic_multiple_tabs.xml b/app/src/main/res/drawable/ic_multiple_tabs.xml new file mode 100644 index 000000000..e57048109 --- /dev/null +++ b/app/src/main/res/drawable/ic_multiple_tabs.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/drawable/media_state_background.xml b/app/src/main/res/drawable/media_state_background.xml new file mode 100644 index 000000000..1dd7ea7ee --- /dev/null +++ b/app/src/main/res/drawable/media_state_background.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/media_state_pause.xml b/app/src/main/res/drawable/media_state_pause.xml new file mode 100644 index 000000000..be9fe79fd --- /dev/null +++ b/app/src/main/res/drawable/media_state_pause.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/play_with_background.xml b/app/src/main/res/drawable/media_state_play.xml similarity index 52% rename from app/src/main/res/drawable/play_with_background.xml rename to app/src/main/res/drawable/media_state_play.xml index db7c29db6..183b83bdd 100644 --- a/app/src/main/res/drawable/play_with_background.xml +++ b/app/src/main/res/drawable/media_state_play.xml @@ -3,16 +3,7 @@ - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - - - - - - - + + android:fillColor="@color/tab_tray_item_media_stroke" + android:pathData="M10,16.363l6,-3.5c0.2,-0.12 0.36,-0.3 0.44,-0.524c0.1,-0.2 0.1,-0.46 0,-0.684c-0.1,-0.22 -0.236,-0.4 -0.44,-0.524l-6,-3.5c-0.2,-0.12 -0.44,-0.16 -0.675,-0.12c-0.23,0.04 -0.44,0.163 -0.6,0.344c-0.15,0.2 -0.234,0.4 -0.233,0.644l0,7c0,0.235 0.084,0.46 0.235,0.641c0.15,0.2 0.36,0.3 0.59,0.34c0.23,0.04 0.5,-0 0.673,-0.1Z"/> diff --git a/app/src/main/res/drawable/pause_with_background.xml b/app/src/main/res/drawable/pause_with_background.xml deleted file mode 100644 index 3df510e23..000000000 --- a/app/src/main/res/drawable/pause_with_background.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/tab_tray_pause_with_background.xml b/app/src/main/res/drawable/tab_tray_pause_with_background.xml deleted file mode 100644 index 337bf611d..000000000 --- a/app/src/main/res/drawable/tab_tray_pause_with_background.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/tab_tray_play_with_background.xml b/app/src/main/res/drawable/tab_tray_play_with_background.xml deleted file mode 100644 index 3694d4c3c..000000000 --- a/app/src/main/res/drawable/tab_tray_play_with_background.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/button_tip_item.xml b/app/src/main/res/layout/button_tip_item.xml index 53ce35925..6f52e18bf 100644 --- a/app/src/main/res/layout/button_tip_item.xml +++ b/app/src/main/res/layout/button_tip_item.xml @@ -2,56 +2,57 @@ - + android:layout_height="wrap_content" + android:background="@drawable/cfr_background_gradient" + android:padding="0dp"> + tools:text="Header text" /> + android:tint="@color/primary_text_dark_theme" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_close" /> + app:layout_constraintTop_toBottomOf="@id/tip_header_text" + tools:text="Tip description" /> + tools:textColor="@color/accent_high_contrast_private_theme" />