From 6a618aa318c45068ea61553e303e8ac6fc6bf2ee Mon Sep 17 00:00:00 2001 From: Sachin Date: Tue, 28 Jul 2020 16:26:18 -0700 Subject: [PATCH 01/74] for #11698 added on demand workmanager initialization (#12739) --- app/src/main/AndroidManifest.xml | 5 +++++ app/src/main/java/org/mozilla/fenix/FenixApplication.kt | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d523a44c5..c2dbb8fbe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -282,6 +282,11 @@ + + diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index ec45fd058..201ba43d1 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -8,9 +8,12 @@ import android.annotation.SuppressLint import android.os.Build import android.os.Build.VERSION.SDK_INT import android.os.StrictMode +import android.util.Log.INFO import androidx.annotation.CallSuper import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.getSystemService +import androidx.work.Configuration.Provider +import androidx.work.Configuration.Builder import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -53,7 +56,7 @@ import org.mozilla.fenix.utils.BrowsersCache * Installs [CrashReporter], initializes [Glean] in fenix builds and setup Megazord in the main process. */ @Suppress("Registered", "TooManyFunctions", "LargeClass") -open class FenixApplication : LocaleAwareApplication() { +open class FenixApplication : LocaleAwareApplication(), Provider { init { recordOnInit() // DO NOT MOVE ANYTHING ABOVE HERE: the timing of this measurement is critical. } @@ -420,4 +423,6 @@ open class FenixApplication : LocaleAwareApplication() { companion object { private const val KINTO_ENDPOINT_PROD = "https://firefox.settings.services.mozilla.com/v1" } + + override fun getWorkManagerConfiguration() = Builder().setMinimumLoggingLevel(INFO).build() } From 537d95c04d381606a9b8481a220acf056361f189 Mon Sep 17 00:00:00 2001 From: sraturi Date: Mon, 29 Jun 2020 15:15:12 -0700 Subject: [PATCH 02/74] for #11830 created class containing the logic for sending AllStartup telemetry logic lint check renamed the intentReceived telemetry to appOpenedAllSource added comments removed unused code moved lifecycle process to AppAllSourceStartTelemetry moved tracking event out of init function lint fix moved appAllStartTelemetry to components added bit more info about the metrics added the onReceivedIntent metric back minor fix change discriptions based on the comments frm MR wrote test cases for AppAllSourceStartTelemetry.kt lint fix test case to mock application going background post rebase: post rebase: fixed nit from comments fixed nit from comments fixed nit from comments lint fix lint fix --- app/metrics.yaml | 30 ++++++ .../java/org/mozilla/fenix/HomeActivity.kt | 21 ++-- .../mozilla/fenix/components/Components.kt | 3 + .../metrics/AppAllSourceStartTelemetry.kt | 59 ++++++++++++ .../components/metrics/GleanMetricsService.kt | 6 +- .../fenix/components/metrics/Metrics.kt | 14 ++- .../customtabs/ExternalAppBrowserActivity.kt | 6 +- .../metrics/AppAllSourceStartTelemetryTest.kt | 95 +++++++++++++++++++ docs/metrics.md | 1 + 9 files changed, 223 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/components/metrics/AppAllSourceStartTelemetry.kt create mode 100644 app/src/test/java/org/mozilla/fenix/components/metrics/AppAllSourceStartTelemetryTest.kt diff --git a/app/metrics.yaml b/app/metrics.yaml index 2c9163ba4..98cf7473a 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -8,6 +8,36 @@ no_lint: - CATEGORY_GENERIC events: + app_opened_all_startup: + type: event + description: | + A user opened the app to the HomeActivity. The HomeActivity + encompasses the home screen, browser screen, settings screen, + collections and other screens in the nav_graph. + This differs from the app_opened probe because it measures all + startups, not just cold startup. Note: There is a short gap + between the time application goes into background and the time + android reports the application going into the background. + Note: This metric does not cover the following cases: + Case # 1 -> a). open a link(for example, gmail) with in-app + Browser (metric report custom_tab startup) b). press home button + c). open gmail again (which brings us back to in app browser). + Step c will not report startup metric. Case # 2 -> a). open fenix + b). press home button c). launch fenix through app switcher/recent + apps. step c will not report startup type. + extra_keys: + source: + description: | + The method used to open Fenix. Possible values are `app_icon`, + `custom_tab`, `link` or `unknown` + bugs: + - https://github.com/mozilla-mobile/fenix/issues/11830 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/12114#pullrequestreview-445245341 + notification_emails: + - esmyth@mozilla.com + - perf-android-fe@mozilla.com + expires: "2020-12-01" app_received_intent: type: event description: | diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index ed1a811c9..a932c4089 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -201,7 +201,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { // record on cold startup safeIntent ?.let(::getIntentAllSource) - ?.also { components.analytics.metrics.track(Event.AppRecievedIntent(it)) } + ?.also { components.analytics.metrics.track(Event.AppReceivedIntent(it)) } } supportActionBar?.hide() @@ -217,9 +217,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { captureSnapshotTelemetryMetrics() + setAppAllStartTelemetry(intent.toSafeIntent()) + StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE. } + protected open fun setAppAllStartTelemetry(safeIntent: SafeIntent) { + components.appAllSourceStartTelemetry.receivedIntentInHomeActivity(safeIntent) + } + @CallSuper override fun onResume() { super.onResume() @@ -281,14 +287,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { ?.also { it.dismissAllowingStateLoss() } } - // If there is a warm or hot startup, onNewIntent method is always called first. // Note: This does not work in case of an user sending an intent with ACTION_VIEW // for example, launch the application, and than use adb to send an intent with // ACTION_VIEW to open a link. In this case, we will get multiple telemetry events. intent .toSafeIntent() .let(::getIntentAllSource) - ?.also { components.analytics.metrics.track(Event.AppRecievedIntent(it)) } + ?.also { components.analytics.metrics.track(Event.AppReceivedIntent(it)) } + + setAppAllStartTelemetry(intent.toSafeIntent()) } /** @@ -412,11 +419,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { } } - protected open fun getIntentAllSource(intent: SafeIntent): Event.AppRecievedIntent.Source? { + protected open fun getIntentAllSource(intent: SafeIntent): Event.AppReceivedIntent.Source? { return when { - intent.isLauncherIntent -> Event.AppRecievedIntent.Source.APP_ICON - intent.action == Intent.ACTION_VIEW -> Event.AppRecievedIntent.Source.LINK - else -> Event.AppRecievedIntent.Source.UNKNOWN + intent.isLauncherIntent -> Event.AppReceivedIntent.Source.APP_ICON + intent.action == Intent.ACTION_VIEW -> Event.AppReceivedIntent.Source.LINK + else -> Event.AppReceivedIntent.Source.UNKNOWN } } diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index eec2fccfe..296ae433d 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -18,6 +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.utils.ClipboardHandler import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Settings @@ -81,6 +82,8 @@ class Components(private val context: Context) { } } + val appAllSourceStartTelemetry by lazy { AppAllSourceStartTelemetry(analytics.metrics) } + @Suppress("MagicNumber") val addonUpdater by lazy { DefaultAddonUpdater(context, AddonUpdater.Frequency(12, TimeUnit.HOURS)) diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/AppAllSourceStartTelemetry.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/AppAllSourceStartTelemetry.kt new file mode 100644 index 000000000..ce730d4f1 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/AppAllSourceStartTelemetry.kt @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.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 + } +} 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 4bf89abbd..0892f4e8d 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 @@ -98,10 +98,14 @@ private val Event.wrapper: EventWrapper<*>? { Events.appOpened.record(it) }, { Events.appOpenedKeys.valueOf(it) } ) - is Event.AppRecievedIntent -> EventWrapper( + is Event.AppReceivedIntent -> EventWrapper( { Events.appReceivedIntent.record(it) }, { Events.appReceivedIntentKeys.valueOf(it) } ) + is Event.AppOpenedAllSourceStartup -> EventWrapper( + { Events.appOpenedAllStartup.record(it) }, + { Events.appOpenedAllStartupKeys.valueOf(it) } + ) is Event.SearchBarTapped -> EventWrapper( { Events.searchBarTapped.record(it) }, { Events.searchBarTappedKeys.valueOf(it) } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt index 22ec2e328..a175f0627 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt @@ -312,11 +312,19 @@ sealed class Event { get() = hashMapOf(Events.appOpenedKeys.source to source.name) } - data class AppRecievedIntent(val source: Source) : Event() { + data class AppReceivedIntent(val source: Source) : Event() { + enum class Source { APP_ICON, LINK, CUSTOM_TAB, UNKNOWN } - override val extras: Map? - get() = hashMapOf(Events.appReceivedIntentKeys.source to source.name) + override val extras: Map? + get() = hashMapOf(Events.appOpenedAllStartupKeys.source to source.name) + } + + data class AppOpenedAllSourceStartup(val source: Source) : Event() { + enum class Source { APP_ICON, LINK, CUSTOM_TAB, UNKNOWN } + + override val extras: Map? + get() = hashMapOf(Events.appOpenedAllStartupKeys.source to source.name) } data class CollectionSaveButtonPressed(val fromScreen: String) : Event() { diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt index 2914f7326..0e079b512 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt @@ -41,10 +41,14 @@ open class ExternalAppBrowserActivity : HomeActivity() { final override fun getIntentSource(intent: SafeIntent) = Event.OpenedApp.Source.CUSTOM_TAB - final override fun getIntentAllSource(intent: SafeIntent) = Event.AppRecievedIntent.Source.CUSTOM_TAB + final override fun getIntentAllSource(intent: SafeIntent) = Event.AppReceivedIntent.Source.CUSTOM_TAB final override fun getIntentSessionId(intent: SafeIntent) = intent.getSessionId() + override fun setAppAllStartTelemetry(safeIntent: SafeIntent) { + components.appAllSourceStartTelemetry.receivedIntentInExternalAppBrowserActivity(safeIntent) + } + override fun getNavDirections( from: BrowserDirection, customTabSessionId: String? diff --git a/app/src/test/java/org/mozilla/fenix/components/metrics/AppAllSourceStartTelemetryTest.kt b/app/src/test/java/org/mozilla/fenix/components/metrics/AppAllSourceStartTelemetryTest.kt new file mode 100644 index 000000000..f411ae029 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/components/metrics/AppAllSourceStartTelemetryTest.kt @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components.metrics + +import android.content.Intent +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.verify +import mozilla.components.support.utils.toSafeIntent +import org.junit.Before +import org.junit.Test + +class AppAllSourceStartTelemetryTest { + + @RelaxedMockK + private lateinit var metricController: MetricController + + @RelaxedMockK + private lateinit var intent: Intent + + private lateinit var appAllSourceStartTelemetry: AppAllSourceStartTelemetry + + @Before + fun setup() { + MockKAnnotations.init(this) + appAllSourceStartTelemetry = AppAllSourceStartTelemetry(metricController) + } + + @Test + fun `WHEN a main launcher intent is received in HomeActivity THEN an app start metric is recorded from app_icon`() { + + every { intent.action } returns Intent.ACTION_MAIN + every { intent.categories.contains(Intent.CATEGORY_LAUNCHER) } returns true + + appAllSourceStartTelemetry.receivedIntentInHomeActivity(intent.toSafeIntent()) + + val validSource = Event.AppOpenedAllSourceStartup.Source.APP_ICON + verify(exactly = 1) { metricController.track(Event.AppOpenedAllSourceStartup(validSource)) } + } + + @Test + fun `WHEN a VIEW intent is received in HomeActivity THEN an app start metric is recorded from link`() { + every { intent.action } returns Intent.ACTION_VIEW + + appAllSourceStartTelemetry.receivedIntentInHomeActivity(intent.toSafeIntent()) + + val validSource = Event.AppOpenedAllSourceStartup.Source.LINK + verify(exactly = 1) { metricController.track(Event.AppOpenedAllSourceStartup(validSource)) } + } + + @Test + fun `WHEN a intent is received in ExternalAppBrowserActivity THEN an app start metric is recorded from custom_tab`() { + val intent = Intent() + + appAllSourceStartTelemetry.receivedIntentInExternalAppBrowserActivity(intent.toSafeIntent()) + + val validSource = Event.AppOpenedAllSourceStartup.Source.CUSTOM_TAB + verify(exactly = 1) { metricController.track(Event.AppOpenedAllSourceStartup(validSource)) } + } + + @Test + fun `GIVEN an app is in the foreground WHEN an intent is received THEN no startup metric is recorded`() { + appAllSourceStartTelemetry.receivedIntentInHomeActivity(intent.toSafeIntent()) + + appAllSourceStartTelemetry.receivedIntentInHomeActivity(intent.toSafeIntent()) + + verify(exactly = 1) { metricController.track(any()) } + } + + @Test + fun `WHEN application goes in background and comes foreground, THEN an app start metric is recorded`() { + // first startup + appAllSourceStartTelemetry.receivedIntentInHomeActivity(intent.toSafeIntent()) + + // mock application going in the background + appAllSourceStartTelemetry.onApplicationOnStop() + + appAllSourceStartTelemetry.receivedIntentInHomeActivity(intent.toSafeIntent()) + + verify(exactly = 2) { metricController.track(any()) } + } + + @Test + fun `WHEN an intent received in HomeActivity is not launcher or does not have VIEW action, THEN an app start is recorded from unknown`() { + every { intent.action } returns Intent.ACTION_MAIN + + appAllSourceStartTelemetry.receivedIntentInHomeActivity(intent.toSafeIntent()) + + val validSource = Event.AppOpenedAllSourceStartup.Source.UNKNOWN + verify(exactly = 1) { metricController.track(Event.AppOpenedAllSourceStartup(validSource)) } + } +} diff --git a/docs/metrics.md b/docs/metrics.md index 68ee68636..bc7162c1f 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -100,6 +100,7 @@ The following metrics are added to the ping: | download_notification.try_again |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user tapped on try again when a download fails in the download notification |[1](https://github.com/mozilla-mobile/fenix/pull/6554)||2020-10-01 | | | error_page.visited_error |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user encountered an error page |[1](https://github.com/mozilla-mobile/fenix/pull/2491#issuecomment-492414486)|
  • error_type: The error type of the error page encountered
|2020-10-01 | | | events.app_opened |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened the app (from cold start, to the homescreen or browser) |[1](https://github.com/mozilla-mobile/fenix/pull/1067#issuecomment-474598673)|
  • source: The method used to open Fenix. Possible values are: `app_icon`, `custom_tab` or `link`
|2020-10-01 | | +| events.app_opened_all_startup |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened the app to the HomeActivity. The HomeActivity encompasses the home screen, browser screen, settings screen, collections and other screens in the nav_graph. This differs from the app_opened probe because it measures all startups, not just cold startup. Note: There is a short gap between the time application goes into background and the time android reports the application going into the background. Note: This metric does not cover the following cases: Case # 1 -> a). open a link(for example, gmail) with in-app Browser (metric report custom_tab startup) b). press home button c). open gmail again (which brings us back to in app browser). Step c will not report startup metric. Case # 2 -> a). open fenix b). press home button c). launch fenix through app switcher/recent apps. step c will not report startup type. |[1](https://github.com/mozilla-mobile/fenix/pull/12114#pullrequestreview-445245341)|
  • source: The method used to open Fenix. Possible values are `app_icon`, `custom_tab`, `link` or `unknown`
|2020-12-01 | | | events.app_received_intent |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The system received an Intent for the HomeActivity. An intent is received an external entity wants to the app to display content. Intents can be received when the app is closed – at which point the app will be opened – or when the app is already opened – at which point the already open app will make changes such as loading a url. This can be used loosely as a heuristic for when the user requested to open the app. The HomeActivity encompasses the home screen and browser screen but may include other screens. This differs from the app_opened probe because it measures all startups, not just cold startup. |[1](https://github.com/mozilla-mobile/fenix/pull/11940/)|
  • source: The method used to open Fenix. Possible values are `app_icon`, `custom_tab`, `link` or `unknown`
|2020-12-01 | | | events.browser_menu_action |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A browser menu item was tapped |[1](https://github.com/mozilla-mobile/fenix/pull/1214#issue-264756708), [2](https://github.com/mozilla-mobile/fenix/pull/5098#issuecomment-529658996), [3](https://github.com/mozilla-mobile/fenix/pull/6310)|
  • item: A string containing the name of the item the user tapped. These items include: Settings, Help, Desktop Site toggle on/off, Find in Page, New Tab, Private Tab, Share, Report Site Issue, Back/Forward button, Reload Button, Quit, Reader Mode On, Reader Mode Off, Open In app, Add To Top Sites, Add-ons Manager, Bookmarks, History
|2020-10-01 | | | events.entered_url |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user entered a url |[1](https://github.com/mozilla-mobile/fenix/pull/1067#issuecomment-474598673)|
  • autocomplete: A boolean that tells us whether the URL was autofilled by an Autocomplete suggestion
|2020-10-01 | | From e2e684b4d452378cf07c70b00f921afe9b5b687a Mon Sep 17 00:00:00 2001 From: mozilla-l10n-automation-bot <54512241+mozilla-l10n-automation-bot@users.noreply.github.com> Date: Tue, 28 Jul 2020 21:37:50 -0400 Subject: [PATCH 03/74] Import l10n. (#13048) --- app/src/main/res/values-es-rES/strings.xml | 50 ++++++++++++++++++---- app/src/main/res/values-gn/strings.xml | 26 +++++++++++ app/src/main/res/values-hi-rIN/strings.xml | 47 ++++++++++++++++---- app/src/main/res/values-hu/strings.xml | 26 +++++++++++ app/src/main/res/values-hy-rAM/strings.xml | 26 +++++++++++ app/src/main/res/values-it/strings.xml | 26 +++++++++++ app/src/main/res/values-kab/strings.xml | 14 ++++++ app/src/main/res/values-kk/strings.xml | 26 +++++++++++ app/src/main/res/values-nb-rNO/strings.xml | 8 ++++ app/src/main/res/values-oc/strings.xml | 39 +++++++++++++++++ app/src/main/res/values-pt-rBR/strings.xml | 26 +++++++++++ app/src/main/res/values-pt-rPT/strings.xml | 14 ++++++ app/src/main/res/values-sk/strings.xml | 26 +++++++++++ app/src/main/res/values-su/strings.xml | 26 +++++++++++ app/src/main/res/values-sv-rSE/strings.xml | 14 +++++- app/src/main/res/values-te/strings.xml | 21 +++++++++ app/src/main/res/values-tr/strings.xml | 26 +++++++++++ app/src/main/res/values-vi/strings.xml | 26 +++++++++++ app/src/main/res/values-zh-rCN/strings.xml | 30 ++++++++++++- app/src/main/res/values-zh-rTW/strings.xml | 26 +++++++++++ 20 files changed, 504 insertions(+), 19 deletions(-) diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 1b5521080..1a8f7ea2e 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -27,6 +27,30 @@ %1$s pestañas abiertas. Toca para cambiar de pestaña. + + %1$d seleccionadas + + Añadir nueva colección + + Nombre + + Seleccionar colección + + Salir del modo de selección múltiple + + Guardar pestañas seleccionadas en la colección + + %1$s seleccionada + + %1$s no seleccionada + + Has salido del modo de selección múltiple + + Has entrado en el modo de selección múltiple, selecciona pestañas para guardar en una colección + + + Seleccionada + %1$s es producido por Mozilla. @@ -151,13 +175,11 @@ Escanear - Atajos + Buscador Ajustes del buscador - - Buscar con - Esta vez, buscar con: + Esta vez, buscar con: Completar enlace desde el portapapeles @@ -267,8 +289,8 @@ Herramientas de desarrollador Depuración remota vía USB - - Mostrar atajos de búsqueda + + Mostrar buscadores Mostrar sugerencias de búsqueda @@ -522,6 +544,9 @@ %1$s (modo privado) + + Guardar + Eliminar historial @@ -733,7 +758,7 @@ Dar nombre a la colección - Agregar nueva colección + Añadir nueva colección Seleccionar todo @@ -1494,4 +1519,13 @@ Vale, entendido - + + + Atajos + + Buscar con + + Esta vez, buscar con: + + Mostrar atajos de búsqueda + diff --git a/app/src/main/res/values-gn/strings.xml b/app/src/main/res/values-gn/strings.xml index d7c545640..750344e21 100644 --- a/app/src/main/res/values-gn/strings.xml +++ b/app/src/main/res/values-gn/strings.xml @@ -26,6 +26,29 @@ %1$s tendayke ijurujáva. Eikutu emoambue hag̃ua tendayke. + + %1$d poravopyre + + Embojuaju ñembyatyha pyahu + + Téra + + Eiporavo ñembyatyha + + Esẽ poravoha hekoetáva ayvúgui + + Eñongatu tendayke poravopyre mbyatyhápe + + Ojeporavo %1$s + + Oñembogue poravo %1$s + + Osẽ poravoha hekoetáva ayvúgui + + Oike poravoha hekoetáva ayvúpe, eiporavo tendayte eñongatu hag̃ua mbyatyhápe + + Poravopyre + %1$s ojapókuri Mozilla. @@ -516,6 +539,9 @@ %1$s (Ayvu Ñemigua) + + Ñongatu + Emboguete tembiasakue diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml index 2b4f8a3c6..a001a6270 100644 --- a/app/src/main/res/values-hi-rIN/strings.xml +++ b/app/src/main/res/values-hi-rIN/strings.xml @@ -25,6 +25,21 @@ %1$s खुले टैब। टैब स्विच करने के लिए टैप करें। + + %1$d चयनित + + नया संग्रह जोड़ें + + नाम + + संग्रह चुनें + + मल्टीसेलेक्ट मोड से बाहर निकलें + + चुने गए टैब को संग्रह में सहेजें + + मल्टीसेलेक्ट मोड से बाहर निकल चुके + %1$s को Mozilla द्वारा निर्मित किया गया है। @@ -151,13 +166,11 @@ स्कैन करें - शॉर्टकट + खोज इंजन खोज इंजन सेटिंग्स - - के साथ खोजें - इस बार, इसके साथ खोजें: + इस बार, इसके साथ खोजें: क्लिपबोर्ड से लिंक भरें @@ -266,8 +279,8 @@ डेवलेपर टूल USB द्वारा रिमोट डीबगिंग - - खोज शॉर्टकट्स दिखाएं + + खोज इंजन दिखाएं खोज सुझाव दिखाएं @@ -516,6 +529,9 @@ %1$s (निजी मोड) + + सहेजें + इतिहास मिटाएं @@ -637,7 +653,7 @@ The first parameter is the host part of the URL of the bookmark deleted, if any --> %1$s मिटाया गया - + बुकमार्क मिटा दिए गए पूर्ववत करें @@ -733,6 +749,8 @@ %d टैब चुना गया टैब सहेजे गए! + + संग्रह सहेजा गया! टैब सहेजे गए! @@ -840,6 +858,8 @@ मना करें क्या आप वाकई %1$s को हटाना चाहते हैं? + + इस टैब को हटाने से संपूर्ण संग्रह मिट जाएगा। आप किसी भी समय नए संग्रह बना सकते हैं। मिटाएं @@ -1288,6 +1308,8 @@ उपयोगकर्ता नाम कॉपी करे साइट कॉपी करे + + ब्राउज़र में साइट खोलें पासवर्ड दिखाएं @@ -1473,4 +1495,13 @@ ठीक है, समझ गए - + + + शॉर्टकट + + के साथ खोजें + + इस बार, इसके साथ खोजें: + + खोज शॉर्टकट्स दिखाएं + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index baff20113..7acc05aa3 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -25,6 +25,29 @@ %1$s nyitott lap. Koppintson a lapváltáshoz. + + %1$d kiválasztva + + Új gyűjtemény hozzáadása + + Név + + Gyűjtemény kiválasztása + + Kilépés a többszörös kiválasztási módból + + Kiválasztott lapok gyűjteménybe mentése + + %1$s kiválasztva + + %1$s kiválasztása visszavonva + + Kilépett a többszörös kiválasztási módból + + Belépett a többszörös kiválasztási módba, válassza ki a lapokat egy gyűjteménybe történő mentéshez + + Kiválasztva + A %1$s a Mozilla terméke. @@ -514,6 +537,9 @@ %1$s (privát mód) + + Mentés + Előzmények törlése diff --git a/app/src/main/res/values-hy-rAM/strings.xml b/app/src/main/res/values-hy-rAM/strings.xml index 65af986e7..7da14f6b2 100644 --- a/app/src/main/res/values-hy-rAM/strings.xml +++ b/app/src/main/res/values-hy-rAM/strings.xml @@ -25,6 +25,29 @@ %1$s բաց ներդիրներ: Հպեք՝ փոխարկելու համար: + + Ընտրված է %1$d + + Ավելացնել նոր հավաքածու + + Անուն + + Ընտրել հավաքածուն + + Փակել բազմակի ընտրման կերպը + + Պահպանել ընտրված ներդիրները հավաքածուում + + Ընտրված է %1$s-ը + + Չընտրված %1$s + + Փակում է բազմակի ընտրման կերպը + + Մուտք բազմակի ընտրման կերպ: Ընտրեք ներդիրներ՝ հավաքածուում պահպանելու համար + + Ընտրված + %1$s-ն արտադրվում է Mozilla-ի կողմից: @@ -508,6 +531,9 @@ %1$s (Գաղտնի կերպ) + + Պահպանել + Ջնջել պատմությունը diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index de28339d1..d7ef5b200 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -27,6 +27,29 @@ Aperte %1$s schede. Tocca per cambiare scheda. + + %1$d selezionate + + Aggiungi nuova raccolta + + Nome + + Seleziona raccolta + + Esci dalla modalità di selezione multipla + + Salva le schede selezionate in una raccolta + + %1$s selezionata + + %1$s deselezionata + + Abbandonata modalità di selezione multipla + + Aperta modalità di selezione multipla, selezionare le schede per salvarle in una raccolta + + Selezionata + %1$s è sviluppato da Mozilla. @@ -522,6 +545,9 @@ %1$s (modalità Navigazione anonima) + + Salva + Cancella cronologia diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index e06544d66..b082b3a74 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -25,6 +25,17 @@ %1$s waccaren yeldin. Sit akken ad tettbeddileḍ gar waccaren. + + %1$d yettwafren + + + Rnu tagrumma tamaynut + + Isem + + + Fren tagrumma + %1$s d afares n Mozilla. @@ -512,6 +523,9 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara %1$s (askar uslig) + + Sekles + Kkes amazray diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index 9270eb873..40ed11982 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -24,6 +24,29 @@ %1$s ашық бет. Беттерді ауыстыру үшін шертіңіз. + + %1$d таңдалды + + Жаңа жинақты қосу + + Аты + + Жинақты таңдау + + Көп таңдау режимінен шығу + + Таңдалған беттерді жинаққа сақтау + + Таңдалды %1$s + + Таңдау алынды %1$s + + Көп таңдау режимінен шығу + + Көп таңдау режиміне кірдік, жинаққа сақтау үшін беттерді таңдаңыз + + Таңдалған + %1$s жасаған Mozilla. @@ -502,6 +525,9 @@ %1$s (жекелік режимі) + + Сақтау + Тарихты өшіру diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index bab02a0e1..44a8ba2ee 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -33,10 +33,18 @@ Velg samling + + Avslutt flervalgsmodus Lagre valgte faner i samlingen Valgte %1$s + + Avmerket %1$s + + Avsluttet flervalgsmodus + + Åpnet flervalgsmodus, velg faner for å lagre i en samling Valgt diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index e67e6691b..4bca5eab7 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -25,6 +25,19 @@ %1$s onglets dobèrts. Tocatz per cambiar d’onglet. + + %1$d seleccionats + + Ajustar una colleccion nòva + + Nom + + Seleccionar una colleccion + + Enregistrar los onglets seleccionats dins la colleccion + + Seleccionat + %1$s es produch per Mozilla. @@ -230,6 +243,8 @@ Servidor de compte Firefox personalizat Servidor Sync personalizat + + Servidor de compte o de sincronozacion modificat. L’aplicacion se tanca per aplicar las modificacions… Compte @@ -305,6 +320,8 @@ La sincronizacion a fracassat. Darrièra sincronizacion corrècta : %s + + La sincronizacion a fracassat. Darrièra sincronizacion : pas jamai Darrièra sincronizacion : %s @@ -318,6 +335,8 @@ Onglets recebuts + + Notificacions d’onglets recebuts d’un Firefox sus un autre periferic. Onglet recebut @@ -343,6 +362,9 @@ Ne saber mai + + Desactivada globalament, anatz als Paramètres per l’activar. + Telemetria @@ -364,6 +386,8 @@ Activar la sincronizacion + + Numerizar lo còdi d’associacion del Firefox de burèu estant Connexion @@ -496,6 +520,9 @@ %1$s (mòde privat) + + Enregistrar + Escafar l’istoric @@ -599,6 +626,8 @@ Modificar lo marcapagina Modificar lo dossièr + + Connectatz-vos per veire los marcapaginas sincronizats. URL @@ -1184,6 +1213,8 @@ Copiar lo nom d’utilizaire Copiar lo site + + Dobrir lo site dins lo navegador Mostrar lo senhal @@ -1317,12 +1348,20 @@ Connectatz-vos amb un compte Firefox Connectatz un autre periferic. + + Tornatz vos autentificar. + + Activatz la sincronizacion dels onglets. + + Vejatz la lista dels onglets dels autres periferics. Se connectar a Sync Arribat a la limita dels sites principals + + Per ajustar un site principal novèl, suprimissètz-ne un autre. Tocatz longament sul site e causissètz suprimir. Òc, plan comprés diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 7dbafae79..dcb7b0153 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -24,6 +24,29 @@ %1$s abas abertas. Toque para alternar abas. + + %1$d selecionadas + + Nova coleção + + Nome + + Selecionar coleção + + Sair do modo de seleção múltipla + + Salvar abas selecionadas em coleção + + %1$s selecionada + + %1$s não selecionada + + Saiu do modo de seleção múltipla + + Entrou no modo de seleção múltipla, selecione abas para salvar em uma coleção + + Selecionada + O %1$s é produzido pela Mozilla. @@ -509,6 +532,9 @@ %1$s (modo privativo) + + Salvar + Limpar histórico diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index cd3a32725..c106bcf56 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -38,6 +38,17 @@ Guardar selecionados separadores na coleção + + %1$s selecionado + + %1$s não-selecionado + + O modo de seleção múltipla foi terminado + + Modo de seleção múltipla iniciado; selecione os separadores a guardar numa coleção + + Selecionado + %1$s é criado pela Mozilla. @@ -525,6 +536,9 @@ %1$s (modo privado) + + Guardar + Eliminar histórico diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 1dd733fe0..d927a9e84 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -26,6 +26,29 @@ 1 otvorených kariet. Ťuknutím prepnete karty. + + Počet vybraných položiek: %1$d + + Pridať novú kolekciu + + Názov + + Výber kolekcie + + Ukončenie režimu viacnásobného výberu + + Uložiť vybrané karty do kolekcie + + Karta %1$s bola vybraná + + Výber karty %1$s bol zrušený + + Režim viacnásobného výberu bol ukončený + + Vstúpili ste do režimu viacnásobného výberu, vyberte si karty, ktoré chcete uložiť do kolekcie + + Vybraná + %1$s vyvíja Mozilla. @@ -517,6 +540,9 @@ %1$s (súkromné prehliadanie) + + Uložiť + Vymazať históriu diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml index c12273970..a155b1da3 100644 --- a/app/src/main/res/values-su/strings.xml +++ b/app/src/main/res/values-su/strings.xml @@ -24,6 +24,29 @@ %1$s tab muka. Toél pikeun ngagilir tab. + + %1$d dipilih + + Tambah koléksi anyar + + Ngaran + + Pilih koléksi + + Kaluar mode lobapilih + + Simpen tab pinilih kana koléksi + + Dipilih %1$s + + Teu dipilih %1$s + + Kaluar mode lobapilih + + Asup mode lobapilih, pilih tab pikeun neundeun ka koléksi + + Dipilih + %1$s dihasilkeun ku Mozilla. @@ -510,6 +533,9 @@ %1$s (Mode Nyamuni) + + Teundeun + Hapus jujutan diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 776df3fc4..7abf7773a 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -26,17 +26,27 @@ %1$s öppna flikar. Tryck för att växla mellan flikar. - %1$d vald + %1$d markerad Lägg till ny samling Namn Välj samling + + Avsluta flervalsläge Spara valda flikar i samlingen + + Markerad %1$s + + Avmarkerad %1$s + + Avslutade flervalsläge + + Öppnade flervalsläge och välj flikar för att spara i en samling - Valda + Markerad %1$s produceras av Mozilla. diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index f7ca022ca..517dd67e5 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -25,9 +25,30 @@ %1$s తెరిచివున్న ట్యాబులు. ట్యాబుల మధ్య మారడానికి తాకండి. + + %1$d ఎంచుకున్నారు + + కొత్త సేకరణను చేర్చు పేరు + + సేకరణను ఎంచుకో + + బహుళఎంపిక రీతి నుండి వైదొలగు + + ఎంచుకున్న ట్యాబులను సేకరణకు భద్రపరుచు + + %1$s‌ను ఎంచుకున్నారు + + %1$s‌ను ఎంచుకోలేదు + + బహుళఎంపిక రీతి నుండి వైదొలగారు + + బహుళఎంపిక రీతిలో ఉన్నారు, సేకరణలో భద్రపరచాల్సిన ట్యాబులను ఎంచుకోండి + + ఎంచుకున్నారు + %1$s‌ని తయారుచేసినది మొజిల్లా. diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 699530192..1b6ccda3a 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -25,6 +25,29 @@ %1$s açık sekme. Sekme değiştirmek için dokunun. + + %1$d sekme seçildi + + Yeni koleksiyon ekle + + Adı + + Koleksiyon seç + + Çoklu seçim modundan çık + + Seçilen sekmeleri koleksiyona kaydet + + %1$s seçildi + + %1$s seçimi kaldırıldı + + Çoklu seçim modundan çıkıldı + + Çoklu seçim moduna girdiniz. Koleksiyona kaydedilecek sekmeleri seçin + + Seçildi + %1$s bir Mozilla ürünüdür. @@ -511,6 +534,9 @@ %1$s (Gizli Mod) + + Kaydet + Geçmişi sil diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index e9c75f5dd..4bc8f7662 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -25,6 +25,29 @@ %1$s thẻ đang mở. Chạm để chuyển thẻ. + + %1$d đã chọn + + Thêm bộ sưu tập mới + + Tên + + Chọn bộ sưu tập + + Thoát chế độ nhiều lựa chọn + + Lưu các thẻ đã chọn vào bộ sưu tập + + Đã chọn %1$s + + Đã bỏ chọn %1$s + + Đã thoát chế độ nhiều lựa chọn + + Đã vào chế độ nhiều lựa chọn, chọn các thẻ để lưu vào bộ sưu tập + + Đã chọn + %1$s được sản xuất bởi Mozilla. @@ -508,6 +531,9 @@ %1$s (Chế độ riêng tư) + + Lưu + Xóa lịch sử diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2ebd74c23..8bc09e8ae 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -28,6 +28,31 @@ 打开了 %1$s 个标签页,点击即可切换。 + + 已选择 %1$d 个标签页 + + 新建收藏集 + + 名称 + + 选择收藏集 + + 退出多选模式 + + 将选择的的标签页保存到收藏集 + + 已选择 %1$s + + 已取消选择 %1$s + + 已退出多选模式 + + + 已进入多选模式,请选择要保存到收藏集的标签页 + + + 已选择 + %1$s —— Mozilla 荣誉出品 @@ -523,6 +548,9 @@ %1$s(隐私模式) + + 保存 + 删除历史记录 @@ -873,7 +901,7 @@ 删除此标签页将删除整个收藏集。您可以随时新建收藏集。 - 要删除 %1$s 吗? + 要删除“%1$s”吗? 删除 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 28cbb7df7..b13bb5b92 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -28,6 +28,29 @@ 開啟了 %1$s 個分頁,點擊即可切換分頁。 + + 已選擇 %1$d 個分頁 + + 新增收藏集 + + 名稱 + + 選擇收藏集 + + 離開分頁多選模式 + + 將選擇的分頁儲存至收藏集 + + 已選擇 %1$s 個分頁 + + 已取消選擇 %1$s 個分頁 + + 已離開分頁多選模式 + + 已進入多選模式,請選擇要儲存到收藏集的分頁 + + 已選取 + %1$s 是由 Mozilla 所打造。 @@ -519,6 +542,9 @@ %1$s(隱私瀏覽模式) + + 儲存 + 刪除瀏覽紀錄 From 4b74ff186be5b16f68fd5fcca74f05f23116c9da Mon Sep 17 00:00:00 2001 From: mcarare Date: Wed, 29 Jul 2020 16:21:01 +0300 Subject: [PATCH 04/74] For #12565: Remove context from DeleteBrowsingDataController constructor --- .../deletebrowsingdata/DeleteAndQuit.kt | 9 +++- .../DeleteBrowsingDataController.kt | 26 +++++----- .../DeleteBrowsingDataFragment.kt | 8 +++- ...DefaultDeleteBrowsingDataControllerTest.kt | 48 +++++++++---------- 4 files changed, 53 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt index 0aac06e0a..b4cf23018 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar +import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings /** @@ -21,7 +22,13 @@ import org.mozilla.fenix.ext.settings fun deleteAndQuit(activity: Activity, coroutineScope: CoroutineScope, snackbar: FenixSnackbar?) { coroutineScope.launch { val settings = activity.settings() - val controller = DefaultDeleteBrowsingDataController(activity, coroutineContext) + val controller = DefaultDeleteBrowsingDataController( + activity.components.useCases.tabsUseCases.removeAllTabs, + activity.components.core.historyStorage, + activity.components.core.permissionStorage, + activity.components.core.engine, + coroutineContext + ) snackbar?.apply { setText(activity.getString(R.string.deleting_browsing_data_in_progress)) diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt index 442929ca6..b8894cc49 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt @@ -4,11 +4,12 @@ package org.mozilla.fenix.settings.deletebrowsingdata -import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import mozilla.components.concept.engine.Engine -import org.mozilla.fenix.ext.components +import mozilla.components.concept.storage.HistoryStorage +import mozilla.components.feature.tabs.TabsUseCases +import org.mozilla.fenix.components.PermissionStorage import kotlin.coroutines.CoroutineContext interface DeleteBrowsingDataController { @@ -21,13 +22,16 @@ interface DeleteBrowsingDataController { } class DefaultDeleteBrowsingDataController( - val context: Context, - val coroutineContext: CoroutineContext = Dispatchers.Main + private val removeAllTabs: TabsUseCases.RemoveAllTabsUseCase, + private val historyStorage: HistoryStorage, + private val permissionStorage: PermissionStorage, + private val engine: Engine, + private val coroutineContext: CoroutineContext = Dispatchers.Main ) : DeleteBrowsingDataController { override suspend fun deleteTabs() { withContext(coroutineContext) { - context.components.useCases.tabsUseCases.removeAllTabs.invoke() + removeAllTabs.invoke() } } @@ -37,14 +41,14 @@ class DefaultDeleteBrowsingDataController( override suspend fun deleteHistoryAndDOMStorages() { withContext(coroutineContext) { - context.components.core.engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES)) + engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES)) } - context.components.core.historyStorage.deleteEverything() + historyStorage.deleteEverything() } override suspend fun deleteCookies() { withContext(coroutineContext) { - context.components.core.engine.clearData( + engine.clearData( Engine.BrowsingData.select( Engine.BrowsingData.COOKIES, Engine.BrowsingData.AUTH_SESSIONS @@ -55,7 +59,7 @@ class DefaultDeleteBrowsingDataController( override suspend fun deleteCachedFiles() { withContext(coroutineContext) { - context.components.core.engine.clearData( + engine.clearData( Engine.BrowsingData.select(Engine.BrowsingData.ALL_CACHES) ) } @@ -63,10 +67,10 @@ class DefaultDeleteBrowsingDataController( override suspend fun deleteSitePermissions() { withContext(coroutineContext) { - context.components.core.engine.clearData( + engine.clearData( Engine.BrowsingData.select(Engine.BrowsingData.ALL_SITE_SETTINGS) ) } - context.components.core.permissionStorage.deleteAllSitePermissions() + permissionStorage.deleteAllSitePermissions() } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt index 73963a161..551341144 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt @@ -25,6 +25,7 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar @@ -40,7 +41,12 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - controller = DefaultDeleteBrowsingDataController(requireContext()) + controller = DefaultDeleteBrowsingDataController( + requireContext().components.useCases.tabsUseCases.removeAllTabs, + requireContext().components.core.historyStorage, + requireContext().components.core.permissionStorage, + requireContext().components.core.engine + ) settings = requireContext().settings() getCheckboxes().forEach { diff --git a/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt index 118edc67c..50be4f470 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt @@ -4,21 +4,21 @@ package org.mozilla.fenix.settings.deletebrowsingdata -import android.content.Context -import io.mockk.Runs -import io.mockk.every -import io.mockk.just import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope.coroutineContext +import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.runBlockingTest import mozilla.components.concept.engine.Engine +import mozilla.components.concept.storage.HistoryStorage +import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.Before import org.junit.Rule import org.junit.Test -import org.mozilla.fenix.ext.components +import org.mozilla.fenix.components.PermissionStorage @OptIn(ExperimentalCoroutinesApi::class) class DefaultDeleteBrowsingDataControllerTest { @@ -26,47 +26,49 @@ class DefaultDeleteBrowsingDataControllerTest { @get:Rule val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher()) - private val context: Context = mockk(relaxed = true) + private var removeAllTabs: TabsUseCases.RemoveAllTabsUseCase = mockk(relaxed = true) + private var historyStorage: HistoryStorage = mockk(relaxed = true) + private var permissionStorage: PermissionStorage = mockk(relaxed = true) + private val engine: Engine = mockk(relaxed = true) private lateinit var controller: DefaultDeleteBrowsingDataController @Before fun setup() { - every { context.components.core.engine.clearData(any()) } just Runs + controller = DefaultDeleteBrowsingDataController( + removeAllTabs = removeAllTabs, + historyStorage = historyStorage, + permissionStorage = permissionStorage, + engine = engine, + coroutineContext = coroutineContext + ) } @Test fun deleteTabs() = runBlockingTest { - controller = DefaultDeleteBrowsingDataController(context, coroutineContext) - every { context.components.useCases.tabsUseCases.removeAllTabs.invoke() } just Runs controller.deleteTabs() verify { - context.components.useCases.tabsUseCases.removeAllTabs.invoke() + removeAllTabs.invoke() } } @Test fun deleteBrowsingData() = runBlockingTest { - controller = DefaultDeleteBrowsingDataController(context, coroutineContext) - every { context.components.core.historyStorage } returns mockk(relaxed = true) - controller.deleteBrowsingData() verify { - context.components.core.engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES)) - context.components.core.historyStorage + engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES)) } + verify { launch { historyStorage.deleteEverything() } } } @Test fun deleteCookies() = runBlockingTest { - controller = DefaultDeleteBrowsingDataController(context, coroutineContext) - controller.deleteCookies() verify { - context.components.core.engine.clearData( + engine.clearData( Engine.BrowsingData.select( Engine.BrowsingData.COOKIES, Engine.BrowsingData.AUTH_SESSIONS @@ -77,25 +79,21 @@ class DefaultDeleteBrowsingDataControllerTest { @Test fun deleteCachedFiles() = runBlockingTest { - controller = DefaultDeleteBrowsingDataController(context, coroutineContext) controller.deleteCachedFiles() verify { - context.components.core.engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.ALL_CACHES)) + engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.ALL_CACHES)) } } @Test fun deleteSitePermissions() = runBlockingTest { - controller = DefaultDeleteBrowsingDataController(context, coroutineContext) - every { context.components.core.permissionStorage.deleteAllSitePermissions() } just Runs - controller.deleteSitePermissions() verify { - context.components.core.engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.ALL_SITE_SETTINGS)) - context.components.core.permissionStorage.deleteAllSitePermissions() + engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.ALL_SITE_SETTINGS)) + permissionStorage.deleteAllSitePermissions() } } } From ac870634ff6742ab09f401b949a41c4d6ae7278b Mon Sep 17 00:00:00 2001 From: Mihai Adrian Carare <48995920+mcarare@users.noreply.github.com> Date: Wed, 29 Jul 2020 18:08:00 +0300 Subject: [PATCH 05/74] For #12533: Align main settings categories according to specs. (#12534) --- .../res/layout/preference_category_main_style.xml | 15 +++++++++++++++ app/src/main/res/xml/preferences.xml | 10 +++++----- 2 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 app/src/main/res/layout/preference_category_main_style.xml diff --git a/app/src/main/res/layout/preference_category_main_style.xml b/app/src/main/res/layout/preference_category_main_style.xml new file mode 100644 index 000000000..f1444e7d2 --- /dev/null +++ b/app/src/main/res/layout/preference_category_main_style.xml @@ -0,0 +1,15 @@ + + + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b9d8a73f4..8e6062573 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -19,7 +19,7 @@ android:title="@string/preferences_category_account" app:iconSpaceReserved="false" app:isPreferenceVisible="false" - android:layout="@layout/preference_cat_style" > + android:layout="@layout/preference_category_main_style" > + android:layout="@layout/preference_category_main_style" > + android:layout="@layout/preference_category_main_style"> + android:layout="@layout/preference_category_main_style"> + android:layout="@layout/preference_category_main_style"> Date: Tue, 28 Jul 2020 18:02:24 -0400 Subject: [PATCH 06/74] For #13042 - Remove bottomSheetCallback in multiselect mode --- .../main/java/org/mozilla/fenix/tabtray/TabTrayView.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 a3ccf9ed9..1c41961ef 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt @@ -70,6 +70,7 @@ class TabTrayView( private val tabTrayItemMenu: TabTrayItemMenu private var menu: BrowserMenu? = null + private val bottomSheetCallback: BottomSheetBehavior.BottomSheetCallback private var tabsTouchHelper: TabsTouchHelper private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor) @@ -83,7 +84,7 @@ class TabTrayView( toggleFabText(isPrivate) - behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() { override fun onSlide(bottomSheet: View, slideOffset: Float) { if (!hasAccessibilityEnabled) { if (slideOffset >= SLIDE_OFFSET) { @@ -100,7 +101,9 @@ class TabTrayView( interactor.onTabTrayDismissed() } } - }) + } + + behavior.addBottomSheetCallback(bottomSheetCallback) val selectedTabIndex = if (!isPrivate) { DEFAULT_TAB_ID @@ -273,6 +276,7 @@ class TabTrayView( view.tabsTray.apply { tabsTouchHelper.attachToRecyclerView(this) } + behavior.addBottomSheetCallback(bottomSheetCallback) toggleUIMultiselect(multiselect = false) @@ -281,6 +285,7 @@ class TabTrayView( is TabTrayDialogFragmentState.Mode.MultiSelect -> { // Disable swipe to delete while in multiselect tabsTouchHelper.attachToRecyclerView(null) + behavior.removeBottomSheetCallback(bottomSheetCallback) toggleUIMultiselect(multiselect = true) From 2c0511c92f0b7648bc2846ccfdc556617860601d Mon Sep 17 00:00:00 2001 From: Mozilla L10n Automation Bot Date: Wed, 29 Jul 2020 15:07:20 +0000 Subject: [PATCH 07/74] Import l10n. --- app/src/main/res/values-en-rGB/strings.xml | 26 ++++++++++++++++++++ app/src/main/res/values-fi/strings.xml | 26 ++++++++++++++++++++ app/src/main/res/values-kab/strings.xml | 9 +++++++ app/src/main/res/values-nb-rNO/strings.xml | 2 +- app/src/main/res/values-nn-rNO/strings.xml | 28 ++++++++++++++++++++++ app/src/main/res/values-uk/strings.xml | 2 +- 6 files changed, 91 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 57ccf4222..d5fcf68e3 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -24,6 +24,29 @@ %1$s open tabs. Tap to switch tabs. + + %1$d selected + + Add new collection + + Name + + Select collection + + Exit multiselect mode + + Save selected tabs to collection + + Selected %1$s + + Unselected %1$s + + Exited multiselect mode + + Entered multiselect mode, select tabs to save to a collection + + Selected + %1$s is produced by Mozilla. @@ -506,6 +529,9 @@ %1$s (Private Mode) + + Save + Delete history diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 3a0bd05a6..d206aa972 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -26,6 +26,27 @@ %1$s avointa välilehteä. Napauta vaihtaaksesi. + + %1$d valittu + + Lisää uusi kokoelma + + Nimi + + Valitse kokoelma + + Poistu monivalintatilasta + + Tallenna valitut välilehdet kokoelmaan + + Valittu %1$s + + Poistuttiin monivalintatilasta + + Siirrytty monivalintatilaan, valitse välilehdet tallentaaksesi kokoelmaan + + Valittu + %1$s on Mozillan tuote. @@ -517,6 +538,9 @@ %1$s (yksityinen tila) + + Tallenna + Poista historia @@ -845,6 +869,8 @@ ESTÄ Poistetaanko %1$s? + + Tämän välilehden poistaminen poistaa koko kokoelman. Voit luoda uusia kokoelmia milloin tahansa. Poistetaanko %1$s? diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index b082b3a74..6ce34004e 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -36,6 +36,15 @@ Fren tagrumma + + Sekles iccaren ittufernen ɣer tegrumma + + %1$s ittufren + + %1$s ur ittufren ara + + Iţufren + %1$s d afares n Mozilla. diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 44a8ba2ee..062805d70 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -40,7 +40,7 @@ Valgte %1$s - Avmerket %1$s + Opphevet merking %1$s Avsluttet flervalgsmodus diff --git a/app/src/main/res/values-nn-rNO/strings.xml b/app/src/main/res/values-nn-rNO/strings.xml index c135de51a..59e1be8ba 100644 --- a/app/src/main/res/values-nn-rNO/strings.xml +++ b/app/src/main/res/values-nn-rNO/strings.xml @@ -27,6 +27,31 @@ %1$s opne faner. Trykk for å byte fane. + + %1$d valde + + Legg til ny samling + + Namn + + Vel samling + + Avslutt fleirvalsmodus + + Lagre valde faner i samlinga + + Valde %1$s + + + Oppheva merking av %1$s + + Avslutta fleirvalmodus + + + Opna fleirvalmodus, vel faner for å lagre i ei samling + + Vald + %1$s er produsert av Mozilla. @@ -513,6 +538,9 @@ %1$s (privatmodus) + + Lagre + Slett historikk diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index dc26ad8cf..4f4eafd84 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -258,7 +258,7 @@ Спеціальний сервер синхронізації - Сервер Облікового запису / Синхронізації Firefox змінено. Вихід з програми для застосування змін… + Сервер Облікового запису/Синхронізації Firefox змінено. Вихід з програми для застосування змін… Обліковий запис From 87a7b6f9ec7d25eb58cd447bc4b4a308f3566f02 Mon Sep 17 00:00:00 2001 From: ekager Date: Wed, 29 Jul 2020 11:08:14 -0400 Subject: [PATCH 08/74] For #12992 - Correctly notifies concat adapters about mode changes --- .../mozilla/fenix/tabtray/SaveToCollectionsButtonAdapter.kt | 6 +++++- app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/SaveToCollectionsButtonAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabtray/SaveToCollectionsButtonAdapter.kt index d9bce5f3b..779e8445a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/SaveToCollectionsButtonAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/SaveToCollectionsButtonAdapter.kt @@ -7,6 +7,7 @@ package org.mozilla.fenix.tabtray import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView @@ -31,7 +32,10 @@ class SaveToCollectionsButtonAdapter( return ViewHolder(itemView, interactor) } - override fun onBindViewHolder(holder: ViewHolder, position: Int) = Unit + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.itemView.isVisible = + interactor.onModeRequested() is TabTrayDialogFragmentState.Mode.Normal + } override fun getItemViewType(position: Int): Int { return ViewHolder.LAYOUT_ID 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 1c41961ef..44b1f609d 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt @@ -413,7 +413,8 @@ class TabTrayView( isPrivateModeSelected ) - this.adapter?.notifyItemRangeChanged(0, tabs.size, true) + collectionsButtonAdapter.notifyItemChanged(0) + tabsAdapter.notifyItemRangeChanged(0, tabs.size, true) } } @@ -425,7 +426,7 @@ class TabTrayView( val selectedBrowserTabIndex = tabs.indexOfFirst { it.id == itemId } - this.adapter?.notifyItemChanged( + tabsAdapter.notifyItemChanged( selectedBrowserTabIndex, true ) } From 048f6a49fe13b49d0b4abbb18675411759fe7753 Mon Sep 17 00:00:00 2001 From: mcarare Date: Wed, 29 Jul 2020 18:25:00 +0300 Subject: [PATCH 09/74] For #12565: Pass passwordsStorage instead of context in constructor. --- .../SavedLoginsStorageController.kt | 32 ++++++++++--------- .../logins/fragment/EditLoginFragment.kt | 3 +- .../logins/fragment/LoginDetailFragment.kt | 2 +- .../logins/fragment/SavedLoginsFragment.kt | 2 +- .../SavedLoginsStorageControllerTest.kt | 28 ++++++++-------- 5 files changed, 34 insertions(+), 33 deletions(-) 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 7df5dd741..e04e1418a 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 @@ -4,7 +4,6 @@ package org.mozilla.fenix.settings.logins.controller -import android.content.Context import android.util.Log import androidx.navigation.NavController import kotlinx.coroutines.CancellationException @@ -18,8 +17,8 @@ import mozilla.components.concept.storage.Login import mozilla.components.service.sync.logins.InvalidRecordException import mozilla.components.service.sync.logins.LoginsStorageException import mozilla.components.service.sync.logins.NoSuchRecordException +import mozilla.components.service.sync.logins.SyncableLoginsStorage import org.mozilla.fenix.R -import org.mozilla.fenix.ext.components import org.mozilla.fenix.settings.logins.LoginsAction import org.mozilla.fenix.settings.logins.LoginsFragmentStore import org.mozilla.fenix.settings.logins.fragment.EditLoginFragmentDirections @@ -29,20 +28,19 @@ import org.mozilla.fenix.settings.logins.mapToSavedLogin * Controller for all saved logins interactions with the password storage component */ open class SavedLoginsStorageController( - private val context: Context, + private val passwordsStorage: SyncableLoginsStorage, private val viewLifecycleScope: CoroutineScope, private val navController: NavController, private val loginsFragmentStore: LoginsFragmentStore ) { - private suspend fun getLogin(loginId: String): Login? = - context.components.core.passwordsStorage.get(loginId) + private suspend fun getLogin(loginId: String): Login? = passwordsStorage.get(loginId) fun delete(loginId: String) { var deleteLoginJob: Deferred? = null val deleteJob = viewLifecycleScope.launch(Dispatchers.IO) { deleteLoginJob = async { - context.components.core.passwordsStorage.delete(loginId) + passwordsStorage.delete(loginId) } deleteLoginJob?.await() withContext(Dispatchers.Main) { @@ -61,7 +59,7 @@ open class SavedLoginsStorageController( viewLifecycleScope.launch(Dispatchers.IO) { saveLoginJob = async { // must retrieve from storage to get the httpsRealm and formActionOrigin - val oldLogin = context.components.core.passwordsStorage.get(loginId) + val oldLogin = passwordsStorage.get(loginId) // Update requires a Login type, which needs at least one of // httpRealm or formActionOrigin @@ -95,16 +93,20 @@ open class SavedLoginsStorageController( private suspend fun save(loginToSave: Login) { try { - context.components.core.passwordsStorage.update(loginToSave) + passwordsStorage.update(loginToSave) } catch (loginException: LoginsStorageException) { when (loginException) { is NoSuchRecordException, is InvalidRecordException -> { - Log.e("Edit login", - "Failed to save edited login.", loginException) + Log.e( + "Edit login", + "Failed to save edited login.", loginException + ) } - else -> Log.e("Edit login", - "Failed to save edited login.", loginException) + else -> Log.e( + "Edit login", + "Failed to save edited login.", loginException + ) } } } @@ -124,7 +126,7 @@ open class SavedLoginsStorageController( val fetchLoginJob = viewLifecycleScope.launch(Dispatchers.IO) { deferredLogin = async { val login = getLogin(loginId) - context.components.core.passwordsStorage.getPotentialDupesIgnoringUsername(login!!) + passwordsStorage.getPotentialDupesIgnoringUsername(login!!) } val fetchedDuplicatesList = deferredLogin?.await() fetchedDuplicatesList?.let { list -> @@ -149,7 +151,7 @@ open class SavedLoginsStorageController( var deferredLogin: Deferred>? = null val fetchLoginJob = viewLifecycleScope.launch(Dispatchers.IO) { deferredLogin = async { - context.components.core.passwordsStorage.list() + passwordsStorage.list() } val fetchedLoginList = deferredLogin?.await() @@ -177,7 +179,7 @@ open class SavedLoginsStorageController( var deferredLogins: Deferred>? = null val fetchLoginsJob = viewLifecycleScope.launch(Dispatchers.IO) { deferredLogins = async { - context.components.core.passwordsStorage.list() + passwordsStorage.list() } val logins = deferredLogins?.await() logins?.let { diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt index 1609e1c6e..2117f93fc 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt @@ -24,6 +24,7 @@ import mozilla.components.support.ktx.android.view.hideKeyboard import org.mozilla.fenix.R import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.redirectToReAuth import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings @@ -79,7 +80,7 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) { interactor = EditLoginInteractor( SavedLoginsStorageController( - context = requireContext(), + passwordsStorage = requireContext().components.core.passwordsStorage, viewLifecycleScope = viewLifecycleOwner.lifecycleScope, navController = findNavController(), loginsFragmentStore = loginsFragmentStore diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/LoginDetailFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/LoginDetailFragment.kt index 06ae9609e..acd6f1320 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/LoginDetailFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/LoginDetailFragment.kt @@ -94,7 +94,7 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) { interactor = LoginDetailInteractor( SavedLoginsStorageController( - context = requireContext(), + passwordsStorage = requireContext().components.core.passwordsStorage, viewLifecycleScope = viewLifecycleOwner.lifecycleScope, navController = findNavController(), loginsFragmentStore = savedLoginsStore diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt index 182a366af..3975bb0fe 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt @@ -101,7 +101,7 @@ class SavedLoginsFragment : Fragment() { ) savedLoginsStorageController = SavedLoginsStorageController( - context = requireContext(), + passwordsStorage = requireContext().components.core.passwordsStorage, viewLifecycleScope = viewLifecycleOwner.lifecycleScope, navController = findNavController(), loginsFragmentStore = savedLoginsStore diff --git a/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsStorageControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsStorageControllerTest.kt index f42e1f774..b023a0cb0 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsStorageControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsStorageControllerTest.kt @@ -4,7 +4,6 @@ package org.mozilla.fenix.settings.logins -import android.content.Context import android.os.Looper import androidx.navigation.NavController import androidx.navigation.NavDestination @@ -17,13 +16,13 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineScope import mozilla.components.concept.storage.Login +import mozilla.components.service.sync.logins.SyncableLoginsStorage import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.R import org.mozilla.fenix.components.Components -import org.mozilla.fenix.ext.components import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController import org.robolectric.Shadows.shadowOf @@ -34,7 +33,7 @@ import org.robolectric.annotation.LooperMode @RunWith(FenixRobolectricTestRunner::class) class SavedLoginsStorageControllerTest { private lateinit var components: Components - private val context: Context = mockk(relaxed = true) + private val passwordsStorage: SyncableLoginsStorage = mockk(relaxed = true) private lateinit var controller: SavedLoginsStorageController private val navController: NavController = mockk(relaxed = true) private val loginsFragmentStore: LoginsFragmentStore = mockk(relaxed = true) @@ -46,13 +45,12 @@ class SavedLoginsStorageControllerTest { every { navController.currentDestination } returns NavDestination("").apply { id = R.id.loginDetailFragment } - coEvery { context.components.core.passwordsStorage.get(any()) } returns loginMock + coEvery { passwordsStorage.get(any()) } returns loginMock every { loginsFragmentStore.dispatch(any()) } returns mockk() - coEvery { context.components.core.passwordsStorage } returns mockk(relaxed = true) components = mockk(relaxed = true) controller = SavedLoginsStorageController( - context = context, + passwordsStorage = passwordsStorage, viewLifecycleScope = MainScope(), navController = navController, loginsFragmentStore = loginsFragmentStore @@ -68,12 +66,12 @@ class SavedLoginsStorageControllerTest { fun `WHEN a login is deleted, THEN navigate back to the previous page`() = runBlocking { val loginId = "id" // mock for deleteLoginJob: Deferred? - coEvery { context.components.core.passwordsStorage.delete(any()) } returns true + coEvery { passwordsStorage.delete(any()) } returns true controller.delete(loginId) shadow() - coVerify { context.components.core.passwordsStorage.delete(loginId) } + coVerify { passwordsStorage.delete(loginId) } } private fun shadow() { @@ -86,11 +84,11 @@ class SavedLoginsStorageControllerTest { fun `WHEN fetching the login list, THEN update the state in the store`() { val loginId = "id" // for deferredLogin: Deferred>? - coEvery { context.components.core.passwordsStorage.list() } returns listOf() + coEvery { passwordsStorage.list() } returns listOf() controller.fetchLoginDetails(loginId) - coVerify { context.components.core.passwordsStorage.list() } + coVerify { passwordsStorage.list() } } @Test @@ -103,11 +101,11 @@ class SavedLoginsStorageControllerTest { httpRealm = "httpRealm", formActionOrigin = "" ) - coEvery { context.components.core.passwordsStorage.get(any()) } returns loginMock + coEvery { passwordsStorage.get(any()) } returns loginMock controller.save(login.guid!!, login.username, login.password) - coVerify { context.components.core.passwordsStorage.get(any()) } + coVerify { passwordsStorage.get(any()) } } @Test @@ -121,11 +119,11 @@ class SavedLoginsStorageControllerTest { formActionOrigin = "" ) - coEvery { context.components.core.passwordsStorage.get(any()) } returns login + coEvery { passwordsStorage.get(any()) } returns login // for deferredLogin: Deferred>? coEvery { - context.components.core.passwordsStorage.getPotentialDupesIgnoringUsername(any()) + passwordsStorage.getPotentialDupesIgnoringUsername(any()) } returns listOf() controller.findPotentialDuplicates(login.guid!!) @@ -133,7 +131,7 @@ class SavedLoginsStorageControllerTest { shadow() coVerify { - context.components.core.passwordsStorage.getPotentialDupesIgnoringUsername(login) + passwordsStorage.getPotentialDupesIgnoringUsername(login) } } } From 579a69b477cbc2aa6e642cd23582b0b96c9a4d69 Mon Sep 17 00:00:00 2001 From: Antti Vainikka Date: Mon, 27 Jul 2020 18:24:33 +0300 Subject: [PATCH 10/74] Closes #12692: Remove www. prefix in tab tray urls Use separate imports instead of a wildcard import Use toShortUrl String extension instead of removing prefix from url host Add missing import --- .../java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 106041472..b2aba6fef 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt @@ -26,7 +26,6 @@ import mozilla.components.feature.media.ext.playIfPaused import mozilla.components.support.base.observer.Observable import mozilla.components.support.images.ImageLoadRequest import mozilla.components.support.images.loader.ImageLoader -import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController @@ -36,6 +35,7 @@ import org.mozilla.fenix.ext.increaseTapArea import org.mozilla.fenix.ext.removeAndDisable import org.mozilla.fenix.ext.removeTouchDelegate import org.mozilla.fenix.ext.showAndEnable +import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.utils.Do import kotlin.math.max @@ -160,7 +160,9 @@ class TabTrayViewHolder( // is done in the toolbar and awesomebar: // https://github.com/mozilla-mobile/fenix/issues/1824 // https://github.com/mozilla-mobile/android-components/issues/6985 - urlView?.text = tab.url.tryGetHostFromUrl().take(MAX_URI_LENGTH) + urlView?.text = tab.url + .toShortUrl(itemView.context.components.publicSuffixList) + .take(MAX_URI_LENGTH) } @VisibleForTesting From 6b6e1956caa9ba560d7294fef28f14627c8c7f48 Mon Sep 17 00:00:00 2001 From: ekager Date: Wed, 29 Jul 2020 11:34:43 -0400 Subject: [PATCH 11/74] For #13071 - Only return to home when session doesn't have parent session to select --- .../java/org/mozilla/fenix/browser/BaseBrowserFragment.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 296769258..4a0bb9799 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -104,7 +104,6 @@ import org.mozilla.fenix.ext.hideToolbar import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents -import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.SharedViewModel import org.mozilla.fenix.theme.ThemeManager @@ -835,14 +834,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session sessionManager.remove(session) true } else { - val isLastSession = - sessionManager.sessionsOfType(private = session.private).count() == 1 if (session.hasParentSession) { sessionManager.remove(session, true) } - // We want to return to home if this removed session was the last session of its type - // and didn't have a parent session to select. - val goToOverview = isLastSession && !session.hasParentSession + // We want to return to home if this session didn't have a parent session to select. + val goToOverview = !session.hasParentSession !goToOverview } } From 2b1122bc77091adea8be0563b952cae28c659303 Mon Sep 17 00:00:00 2001 From: Sawyer Blatz Date: Mon, 27 Jul 2020 14:10:30 -0700 Subject: [PATCH 12/74] For #11531: Add search widget installed leanplum event --- .../mozilla/fenix/components/metrics/GleanMetricsService.kt | 1 + .../mozilla/fenix/components/metrics/LeanplumMetricsService.kt | 1 + .../main/java/org/mozilla/fenix/components/metrics/Metrics.kt | 1 + .../main/java/org/mozilla/gecko/search/SearchWidgetProvider.kt | 3 +++ 4 files changed, 6 insertions(+) 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 0892f4e8d..f7cf4f504 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 @@ -665,6 +665,7 @@ private val Event.wrapper: EventWrapper<*>? is Event.DismissedOnboarding -> null is Event.FennecToFenixMigrated -> null is Event.AddonInstalled -> null + is Event.SearchWidgetInstalled -> null } class GleanMetricsService(private val context: Context) : MetricsService { diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt index fd00fd9ef..c4b1da1ee 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt @@ -39,6 +39,7 @@ private val Event.name: String? is Event.DismissedOnboarding -> "E_Dismissed_Onboarding" is Event.FennecToFenixMigrated -> "E_Fennec_To_Fenix_Migrated" is Event.AddonInstalled -> "E_Addon_Installed" + is Event.SearchWidgetInstalled -> "E_Search_Widget_Added" // Do not track other events in Leanplum else -> null diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt index a175f0627..697991068 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt @@ -176,6 +176,7 @@ sealed class Event { object SearchWidgetCFRCanceled : Event() object SearchWidgetCFRNotNowPressed : Event() object SearchWidgetCFRAddWidgetPressed : Event() + object SearchWidgetInstalled : Event() object OnboardingAutoSignIn : Event() object OnboardingManualSignIn : Event() object OnboardingPrivacyNotice : Event() diff --git a/app/src/main/java/org/mozilla/gecko/search/SearchWidgetProvider.kt b/app/src/main/java/org/mozilla/gecko/search/SearchWidgetProvider.kt index b3650ff4d..57a62160d 100644 --- a/app/src/main/java/org/mozilla/gecko/search/SearchWidgetProvider.kt +++ b/app/src/main/java/org/mozilla/gecko/search/SearchWidgetProvider.kt @@ -23,6 +23,8 @@ import androidx.core.graphics.drawable.toBitmap import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.intent.StartSearchIntentProcessor import org.mozilla.fenix.widget.VoiceSearchActivity @@ -36,6 +38,7 @@ class SearchWidgetProvider : AppWidgetProvider() { override fun onEnabled(context: Context) { context.settings().addSearchWidgetInstalled(1) + context.metrics.track(Event.SearchWidgetInstalled) } override fun onDeleted(context: Context, appWidgetIds: IntArray) { From 8bfc4935687dbc7220403c8a873d04260c513aaa Mon Sep 17 00:00:00 2001 From: Sawyer Blatz Date: Mon, 27 Jul 2020 14:24:02 -0700 Subject: [PATCH 13/74] For #12103: Add ChangedDefaultBrowser event for leanplum --- .../main/java/org/mozilla/fenix/HomeActivity.kt | 14 ++++++++++++++ .../components/metrics/GleanMetricsService.kt | 1 + .../components/metrics/LeanplumMetricsService.kt | 1 + .../mozilla/fenix/components/metrics/Metrics.kt | 1 + .../fenix/settings/DefaultBrowserPreference.kt | 2 ++ .../main/java/org/mozilla/fenix/utils/Settings.kt | 9 +++++++++ app/src/main/res/values/preference_keys.xml | 2 ++ 7 files changed, 30 insertions(+) diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index a932c4089..5f07c6095 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -31,6 +31,7 @@ import androidx.navigation.ui.NavigationUI import kotlinx.android.synthetic.main.activity_home.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -68,6 +69,7 @@ import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.alreadyOnDestination import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.resetPoliciesAfter import org.mozilla.fenix.ext.settings @@ -243,12 +245,24 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { } } } + + // Launch this on a background thread so as not to affect startup performance + lifecycleScope.launch(IO) { + if (settings().wasDefaultBrowserOnLastPause != settings().isDefaultBrowser()) { + metrics.track(Event.ChangedToDefaultBrowser) + } + } } final override fun onPause() { if (settings().lastKnownMode.isPrivate) { window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) } + + if (settings().wasDefaultBrowserOnLastPause != settings().isDefaultBrowser()) { + settings().wasDefaultBrowserOnLastPause = settings().isDefaultBrowser() + } + super.onPause() // Every time the application goes into the background, it is possible that the user 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 f7cf4f504..755a3095a 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 @@ -666,6 +666,7 @@ private val Event.wrapper: EventWrapper<*>? is Event.FennecToFenixMigrated -> null is Event.AddonInstalled -> null is Event.SearchWidgetInstalled -> null + is Event.ChangedToDefaultBrowser -> null } class GleanMetricsService(private val context: Context) : MetricsService { diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt index c4b1da1ee..8675ccb5d 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt @@ -40,6 +40,7 @@ private val Event.name: String? is Event.FennecToFenixMigrated -> "E_Fennec_To_Fenix_Migrated" is Event.AddonInstalled -> "E_Addon_Installed" is Event.SearchWidgetInstalled -> "E_Search_Widget_Added" + is Event.ChangedToDefaultBrowser -> "E_Changed_Default_To_Fenix" // Do not track other events in Leanplum else -> null diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt index 697991068..676ba80a0 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt @@ -183,6 +183,7 @@ sealed class Event { object OnboardingPrivateBrowsing : Event() object OnboardingWhatsNew : Event() object OnboardingFinish : Event() + object ChangedToDefaultBrowser : Event() object ContextualHintETPDisplayed : Event() object ContextualHintETPDismissed : Event() diff --git a/app/src/main/java/org/mozilla/fenix/settings/DefaultBrowserPreference.kt b/app/src/main/java/org/mozilla/fenix/settings/DefaultBrowserPreference.kt index c2ef16ebb..2aa21de6e 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/DefaultBrowserPreference.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/DefaultBrowserPreference.kt @@ -10,6 +10,8 @@ import android.widget.Switch import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.utils.BrowsersCache class DefaultBrowserPreference @JvmOverloads constructor( 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 defa8901d..cac21f0b5 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -324,6 +324,15 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = true ) + /** + * Caches the last known "is default browser" state when the app was paused. + * For an up to do date state use `isDefaultBrowser` instead. + */ + var wasDefaultBrowserOnLastPause by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_default_browser), + default = isDefaultBrowser() + ) + fun isDefaultBrowser(): Boolean { val browsers = BrowsersCache.all(appContext) return browsers.isDefaultBrowser diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 08f64e202..56aba91dd 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -186,5 +186,7 @@ pref_key_is_in_search_widget_experiment pref_key_show_search_widget_cfr + pref_key_default_browser + pref_key_login_exceptions From bb03049fb5782239200111a444d2486f71e43c46 Mon Sep 17 00:00:00 2001 From: Sawyer Blatz Date: Mon, 27 Jul 2020 14:36:48 -0700 Subject: [PATCH 14/74] Add documentation --- app/src/main/java/org/mozilla/fenix/HomeActivity.kt | 8 ++++++-- .../mozilla/fenix/settings/DefaultBrowserPreference.kt | 2 -- docs/mma.md | 10 ++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 5f07c6095..5cf4e4e3c 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -248,7 +248,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { // Launch this on a background thread so as not to affect startup performance lifecycleScope.launch(IO) { - if (settings().wasDefaultBrowserOnLastPause != settings().isDefaultBrowser()) { + if ( + settings().isDefaultBrowser() && + settings().wasDefaultBrowserOnLastPause != settings().isDefaultBrowser() + ) { metrics.track(Event.ChangedToDefaultBrowser) } } @@ -259,7 +262,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) } - if (settings().wasDefaultBrowserOnLastPause != settings().isDefaultBrowser()) { + if (settings().wasDefaultBrowserOnLastPause != settings().isDefaultBrowser() + ) { settings().wasDefaultBrowserOnLastPause = settings().isDefaultBrowser() } diff --git a/app/src/main/java/org/mozilla/fenix/settings/DefaultBrowserPreference.kt b/app/src/main/java/org/mozilla/fenix/settings/DefaultBrowserPreference.kt index 2aa21de6e..c2ef16ebb 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/DefaultBrowserPreference.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/DefaultBrowserPreference.kt @@ -10,8 +10,6 @@ import android.widget.Switch import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import org.mozilla.fenix.R -import org.mozilla.fenix.components.metrics.Event -import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.utils.BrowsersCache class DefaultBrowserPreference @JvmOverloads constructor( diff --git a/docs/mma.md b/docs/mma.md index 1340ff105..a1ac8e7a8 100644 --- a/docs/mma.md +++ b/docs/mma.md @@ -246,6 +246,16 @@ Here is the list of current Events sent, which can be found here in the code bas The user has installed an addon from the addon management page. #12136 + + `E_Search_Widget_Added` + The user has installed the search widget to their homescreen. + #13003 + + + `E_Changed_Default_To_Fenix` + The user has changed their default browser to Fenix while Fenix was in the background and then resumed the app + #13003 + Deep links From 19ffb84cf97e42cefbfce151ffe351d8e030d286 Mon Sep 17 00:00:00 2001 From: ekager Date: Wed, 29 Jul 2020 12:37:08 -0400 Subject: [PATCH 15/74] For #11654 - Adds leanplum ETP changed event --- .../fenix/components/metrics/LeanplumMetricsService.kt | 1 + docs/mma.md | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt index 8675ccb5d..99a0d4a50 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt @@ -41,6 +41,7 @@ private val Event.name: String? is Event.AddonInstalled -> "E_Addon_Installed" is Event.SearchWidgetInstalled -> "E_Search_Widget_Added" is Event.ChangedToDefaultBrowser -> "E_Changed_Default_To_Fenix" + is Event.TrackingProtectionSettingChanged -> "E_Changed_ETP" // Do not track other events in Leanplum else -> null diff --git a/docs/mma.md b/docs/mma.md index a1ac8e7a8..14d8e2be7 100644 --- a/docs/mma.md +++ b/docs/mma.md @@ -253,7 +253,12 @@ Here is the list of current Events sent, which can be found here in the code bas `E_Changed_Default_To_Fenix` - The user has changed their default browser to Fenix while Fenix was in the background and then resumed the app + The user has changed their default browser to Fenix while Fenix was in the background and then resumed the app. + #13003 + + + `E_Changed_ETP` + The user has changed their enhanched tracking protection setting. #13003 From 9066d0bf20aa08ea2a0bf45c4e68bc644f68d68b Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Wed, 29 Jul 2020 10:55:03 -0700 Subject: [PATCH 16/74] Switch to groovy in buildSrc (#12329) --- buildSrc/{build.gradle.kts => build.gradle} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename buildSrc/{build.gradle.kts => build.gradle} (82%) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle similarity index 82% rename from buildSrc/build.gradle.kts rename to buildSrc/build.gradle index f7bcc37af..508a8d43d 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ plugins { - `kotlin-dsl` + id "org.gradle.kotlin.kotlin-dsl" version "1.3.6" } repositories { From 1497e4886edd0c46624adc4bed3f028fc81bfdb8 Mon Sep 17 00:00:00 2001 From: ekager Date: Wed, 29 Jul 2020 13:29:16 -0400 Subject: [PATCH 17/74] For #13084 - Update SavedLoginsStorageControllerTest --- .../SavedLoginsStorageControllerTest.kt | 125 +++++++++++++----- 1 file changed, 89 insertions(+), 36 deletions(-) diff --git a/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsStorageControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsStorageControllerTest.kt index b023a0cb0..f9cb599dc 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsStorageControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsStorageControllerTest.kt @@ -4,35 +4,38 @@ package org.mozilla.fenix.settings.logins -import android.os.Looper import androidx.navigation.NavController import androidx.navigation.NavDestination +import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every +import io.mockk.just import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.MainScope import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.TestCoroutineScope import mozilla.components.concept.storage.Login import mozilla.components.service.sync.logins.SyncableLoginsStorage +import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.R -import org.mozilla.fenix.components.Components +import org.mozilla.fenix.ext.directionsEq import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController -import org.robolectric.Shadows.shadowOf -import org.robolectric.annotation.LooperMode +import org.mozilla.fenix.settings.logins.fragment.EditLoginFragmentDirections @ExperimentalCoroutinesApi -@LooperMode(LooperMode.Mode.PAUSED) @RunWith(FenixRobolectricTestRunner::class) class SavedLoginsStorageControllerTest { - private lateinit var components: Components + @get:Rule + val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher()) + private val passwordsStorage: SyncableLoginsStorage = mockk(relaxed = true) private lateinit var controller: SavedLoginsStorageController private val navController: NavController = mockk(relaxed = true) @@ -47,11 +50,10 @@ class SavedLoginsStorageControllerTest { } coEvery { passwordsStorage.get(any()) } returns loginMock every { loginsFragmentStore.dispatch(any()) } returns mockk() - components = mockk(relaxed = true) controller = SavedLoginsStorageController( passwordsStorage = passwordsStorage, - viewLifecycleScope = MainScope(), + viewLifecycleScope = scope, navController = navController, loginsFragmentStore = loginsFragmentStore ) @@ -65,34 +67,17 @@ class SavedLoginsStorageControllerTest { @Test fun `WHEN a login is deleted, THEN navigate back to the previous page`() = runBlocking { val loginId = "id" - // mock for deleteLoginJob: Deferred? coEvery { passwordsStorage.delete(any()) } returns true controller.delete(loginId) - shadow() - - coVerify { passwordsStorage.delete(loginId) } - } - - private fun shadow() { - // solves issue with Roboelectric v4.3 and SDK 28 - // https://github.com/robolectric/robolectric/issues/5356 - shadowOf(Looper.getMainLooper()).idle() + coVerify { + passwordsStorage.delete(loginId) + navController.popBackStack(R.id.savedLoginsFragment, false) + } } @Test fun `WHEN fetching the login list, THEN update the state in the store`() { - val loginId = "id" - // for deferredLogin: Deferred>? - coEvery { passwordsStorage.list() } returns listOf() - - controller.fetchLoginDetails(loginId) - - coVerify { passwordsStorage.list() } - } - - @Test - fun `WHEN saving an update to an item, THEN navigate to login detail view`() { val login = Login( guid = "id", origin = "https://www.test.co.gov.org", @@ -101,11 +86,64 @@ class SavedLoginsStorageControllerTest { httpRealm = "httpRealm", formActionOrigin = "" ) - coEvery { passwordsStorage.get(any()) } returns loginMock + coEvery { passwordsStorage.list() } returns listOf(login) - controller.save(login.guid!!, login.username, login.password) + controller.fetchLoginDetails(login.guid!!) - coVerify { passwordsStorage.get(any()) } + val expectedLogin = login.mapToSavedLogin() + + coVerify { + passwordsStorage.list() + loginsFragmentStore.dispatch( + LoginsAction.UpdateCurrentLogin( + expectedLogin + ) + ) + } + } + + @Test + fun `WHEN saving an update to an item, THEN navigate to login detail view`() { + val oldLogin = Login( + guid = "id", + origin = "https://www.test.co.gov.org", + username = "user123", + password = "securePassword1", + httpRealm = "httpRealm", + formActionOrigin = "" + ) + + coEvery { passwordsStorage.get(any()) } returns oldLogin + coEvery { passwordsStorage.update(any()) } just Runs + + controller.save(oldLogin.guid!!, "newUsername", "newPassword") + + val directions = + EditLoginFragmentDirections.actionEditLoginFragmentToLoginDetailFragment( + oldLogin.guid!! + ) + + val newLogin = Login( + guid = "id", + origin = "https://www.test.co.gov.org", + username = "newUsername", + password = "newPassword", + httpRealm = "httpRealm", + formActionOrigin = "" + ) + + val expectedNewList = listOf(newLogin.mapToSavedLogin()) + + coVerify { + passwordsStorage.get(oldLogin.guid!!) + passwordsStorage.update(newLogin) + loginsFragmentStore.dispatch( + LoginsAction.UpdateLoginsList( + expectedNewList + ) + ) + navController.navigate(directionsEq(directions)) + } } @Test @@ -119,19 +157,34 @@ class SavedLoginsStorageControllerTest { formActionOrigin = "" ) + val login2 = Login( + guid = "id2", + origin = "https://www.test.co.gov.org", + username = "user1234", + password = "securePassword1", + httpRealm = "httpRealm", + formActionOrigin = "" + ) + coEvery { passwordsStorage.get(any()) } returns login - // for deferredLogin: Deferred>? + val dupeList = listOf(login2) + coEvery { passwordsStorage.getPotentialDupesIgnoringUsername(any()) - } returns listOf() + } returns dupeList controller.findPotentialDuplicates(login.guid!!) - shadow() + val expectedDupeList = dupeList.map { it.mapToSavedLogin() } coVerify { passwordsStorage.getPotentialDupesIgnoringUsername(login) + loginsFragmentStore.dispatch( + LoginsAction.ListOfDupes( + dupeList = expectedDupeList + ) + ) } } } From 4b646c03cb27a3c3885807906d0ef7cf462868d9 Mon Sep 17 00:00:00 2001 From: Sawyer Blatz Date: Tue, 28 Jul 2020 17:22:05 -0700 Subject: [PATCH 18/74] For #9730: Add Login Dialog Prompt telemetry --- app/metrics.yaml | 46 +++++++++++++++++++ .../components/metrics/GleanMetricsService.kt | 13 ++++++ .../fenix/components/metrics/Metrics.kt | 11 +++++ docs/metrics.md | 4 ++ 4 files changed, 74 insertions(+) diff --git a/app/metrics.yaml b/app/metrics.yaml index 98cf7473a..77b359729 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -471,6 +471,52 @@ context_menu: - fenix-core@mozilla.com expires: "2020-10-01" +login_dialog: + displayed: + type: event + description: | + The login dialog prompt was displayed + bugs: + - https://github.com/mozilla-mobile/fenix/issues/9730 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/13050 + notification_emails: + - fenix-core@mozilla.com + expires: "2021-02-01" + cancelled: + type: event + description: | + The login dialog prompt was cancelled + bugs: + - https://github.com/mozilla-mobile/fenix/issues/9730 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/13050 + notification_emails: + - fenix-core@mozilla.com + expires: "2021-02-01" + saved: + type: event + description: | + The login dialog prompt "save" button was pressed + bugs: + - https://github.com/mozilla-mobile/fenix/issues/9730 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/13050 + notification_emails: + - fenix-core@mozilla.com + expires: "2021-02-01" + never_save: + type: event + description: | + The login dialog prompt "never save" button was pressed + bugs: + - https://github.com/mozilla-mobile/fenix/issues/9730 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/13050 + notification_emails: + - fenix-core@mozilla.com + expires: "2021-02-01" + find_in_page: opened: type: 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 755a3095a..55ebab4f2 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 @@ -24,6 +24,7 @@ import org.mozilla.fenix.GleanMetrics.ErrorPage import org.mozilla.fenix.GleanMetrics.Events 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.MediaNotification import org.mozilla.fenix.GleanMetrics.MediaState @@ -140,6 +141,18 @@ private val Event.wrapper: EventWrapper<*>? { SearchShortcuts.selected.record(it) }, { SearchShortcuts.selectedKeys.valueOf(it) } ) + is Event.LoginDialogPromptDisplayed -> EventWrapper( + { LoginDialog.displayed.record(it) } + ) + is Event.LoginDialogPromptCancelled -> EventWrapper( + { LoginDialog.cancelled.record(it) } + ) + is Event.LoginDialogPromptSave -> EventWrapper( + { LoginDialog.saved.record(it) } + ) + is Event.LoginDialogPromptNeverSave -> EventWrapper( + { LoginDialog.neverSave.record(it) } + ) is Event.FindInPageOpened -> EventWrapper( { FindInPage.opened.record(it) } ) diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt index 676ba80a0..f6543991a 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt @@ -21,6 +21,7 @@ import mozilla.components.feature.customtabs.CustomTabsFacts import mozilla.components.feature.downloads.facts.DownloadsFacts import mozilla.components.feature.findinpage.facts.FindInPageFacts import mozilla.components.feature.media.facts.MediaFacts +import mozilla.components.feature.prompts.dialog.LoginDialogFacts import mozilla.components.support.base.Component import mozilla.components.support.base.facts.Action import mozilla.components.support.base.facts.Fact @@ -185,6 +186,11 @@ sealed class Event { object OnboardingFinish : Event() object ChangedToDefaultBrowser : Event() + object LoginDialogPromptDisplayed : Event() + object LoginDialogPromptCancelled : Event() + object LoginDialogPromptSave : Event() + object LoginDialogPromptNeverSave : Event() + object ContextualHintETPDisplayed : Event() object ContextualHintETPDismissed : Event() object ContextualHintETPOutsideTap : Event() @@ -514,6 +520,11 @@ sealed class Event { } private fun Fact.toEvent(): Event? = when (Pair(component, item)) { + Component.FEATURE_PROMPTS to LoginDialogFacts.Items.DISPLAY -> Event.LoginDialogPromptDisplayed + Component.FEATURE_PROMPTS to LoginDialogFacts.Items.CANCEL -> Event.LoginDialogPromptCancelled + Component.FEATURE_PROMPTS to LoginDialogFacts.Items.NEVER_SAVE -> Event.LoginDialogPromptNeverSave + Component.FEATURE_PROMPTS to LoginDialogFacts.Items.SAVE -> Event.LoginDialogPromptSave + Component.FEATURE_FINDINPAGE to FindInPageFacts.Items.CLOSE -> Event.FindInPageClosed Component.FEATURE_FINDINPAGE to FindInPageFacts.Items.INPUT -> Event.FindInPageSearchCommitted Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.ITEM -> { diff --git a/docs/metrics.md b/docs/metrics.md index bc7162c1f..0ea774ade 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -118,6 +118,10 @@ The following metrics are added to the ping: | history.removed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user removed a history item |[1](https://github.com/mozilla-mobile/fenix/pull/3940)||2020-10-01 | | | history.removed_all |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user removed all history items |[1](https://github.com/mozilla-mobile/fenix/pull/3940)||2020-10-01 | | | history.shared |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user shared a history item |[1](https://github.com/mozilla-mobile/fenix/pull/3940)||2020-10-01 | | +| login_dialog.cancelled |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The login dialog prompt was cancelled |[1](https://github.com/mozilla-mobile/fenix/pull/13050)||2021-02-01 | | +| login_dialog.displayed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The login dialog prompt was displayed |[1](https://github.com/mozilla-mobile/fenix/pull/13050)||2021-02-01 | | +| login_dialog.never_save |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The login dialog prompt "never save" button was pressed |[1](https://github.com/mozilla-mobile/fenix/pull/13050)||2021-02-01 | | +| login_dialog.saved |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The login dialog prompt "save" button was pressed |[1](https://github.com/mozilla-mobile/fenix/pull/13050)||2021-02-01 | | | logins.copy_login |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user copied a piece of a login in saved logins |[1](https://github.com/mozilla-mobile/fenix/pull/6352)||2020-10-01 | | | logins.delete_saved_login |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user confirms delete of a saved login |[1](https://github.com/mozilla-mobile/fenix/issues/11208)||2020-10-01 | | | logins.open_individual_login |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user accessed an individual login in saved logins |[1](https://github.com/mozilla-mobile/fenix/pull/6352)||2020-10-01 | | From d5b105097ee529a69e6b58df437f43561ee1d10d Mon Sep 17 00:00:00 2001 From: mozilla-l10n-automation-bot <54512241+mozilla-l10n-automation-bot@users.noreply.github.com> Date: Wed, 29 Jul 2020 16:51:01 -0400 Subject: [PATCH 19/74] Import l10n. (#13088) --- app/src/main/res/values-kab/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 6ce34004e..cb7f8862b 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -36,12 +36,18 @@ Fren tagrumma + + Ffeɣ seg uskar n uget-afran Sekles iccaren ittufernen ɣer tegrumma %1$s ittufren %1$s ur ittufren ara + + Iffeɣ seg uskar n uget-afran + + Ikcem ɣer uskar n uget-afran, fren iccaren akken ad teskelseḍ ɣer tegrumma Iţufren From a8fd37740d8f113b15c55654439915e099002050 Mon Sep 17 00:00:00 2001 From: Kainalu Hagiwara Date: Wed, 29 Jul 2020 09:52:01 -0700 Subject: [PATCH 20/74] For #13066 - Translate locale codes to Android specific variants before filtering --- automation/taskcluster/l10n/locales.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/automation/taskcluster/l10n/locales.py b/automation/taskcluster/l10n/locales.py index 868c71372..4cf694e11 100644 --- a/automation/taskcluster/l10n/locales.py +++ b/automation/taskcluster/l10n/locales.py @@ -11,11 +11,21 @@ import re OPEN_LOCALES = "locales = [" CLOSE_LOCALES = "]" +# Android uses non-standard locale codes, these are the mappings back and forth +# See Legacy language codes in https://developer.android.com/reference/java/util/Locale.html +ANDROID_LEGACY_MAP = { + 'he': 'iw', + 'id': 'in', + 'yi': 'ji' +} + def trim_to_locale(str): match = re.search('\s*"([a-z]+-?[A-Z]*)",\s*', str) if not match: raise Exception("Failed parsing locale found in l10n.toml: " + str) - return match.group(1) + + locale = match.group(1) + return ANDROID_LEGACY_MAP.get(locale, locale) # This file is a dumb parser that converts values from '/l10n-release.toml' to be easily From 57c795563700be06f818a4b1b9c452a2c72b63b2 Mon Sep 17 00:00:00 2001 From: Kainalu Hagiwara Date: Tue, 28 Jul 2020 11:25:17 -0700 Subject: [PATCH 21/74] For #12865, #12990 - Disable swipe to switch tabs gesture when the keyboard is visible. --- .../fenix/browser/ToolbarGestureHandler.kt | 26 ++---- .../main/java/org/mozilla/fenix/ext/View.kt | 62 ++++++++++++++ .../java/org/mozilla/fenix/ext/ViewTest.kt | 82 +++++++++++++++++++ 3 files changed, 153 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt b/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt index 11fe97a65..747c5cb40 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt @@ -9,7 +9,6 @@ import android.animation.AnimatorListenerAdapter import android.app.Activity import android.graphics.PointF import android.graphics.Rect -import android.os.Build import android.util.TypedValue import android.view.View import android.view.ViewConfiguration @@ -17,7 +16,6 @@ import androidx.annotation.Dimension import androidx.annotation.Dimension.DP import androidx.core.graphics.contains import androidx.core.graphics.toPoint -import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.FlingAnimation @@ -25,6 +23,8 @@ import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.support.ktx.android.view.getRectWithViewLocation +import org.mozilla.fenix.ext.getWindowInsets +import org.mozilla.fenix.ext.isKeyboardVisible import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.settings import kotlin.math.abs @@ -35,7 +35,6 @@ import kotlin.math.min * Handles intercepting touch events on the toolbar for swipe gestures and executes the * necessary animations. */ -@Suppress("LargeClass", "TooManyFunctions") class ToolbarGestureHandler( private val activity: Activity, private val contentLayout: View, @@ -56,18 +55,6 @@ class ToolbarGestureHandler( private val windowWidth: Int get() = activity.resources.displayMetrics.widthPixels - private val windowInsets: WindowInsetsCompat? - get() = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // In theory, the rootWindowInsets should exist at this point but if the decorView is - // not attached for some reason we'll get a NullPointerException without the check. - activity.window.decorView.rootWindowInsets?.let { - WindowInsetsCompat.toWindowInsetsCompat(it) - } - } else { - null - } - private val previewOffset = PREVIEW_OFFSET.dpToPx(activity.resources.displayMetrics) private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop @@ -89,7 +76,12 @@ class ToolbarGestureHandler( GestureDirection.LEFT_TO_RIGHT } - return if (start.isInToolbar() && abs(dx) > touchSlop && abs(dy) < abs(dx)) { + return if ( + !activity.window.decorView.isKeyboardVisible() && + start.isInToolbar() && + abs(dx) > touchSlop && + abs(dy) < abs(dx) + ) { preparePreview(getDestination()) true } else { @@ -313,7 +305,7 @@ class ToolbarGestureHandler( val toolbarLocation = toolbarLayout.getRectWithViewLocation() // In Android 10, the system gesture touch area overlaps the bottom of the toolbar, so // lets make our swipe area taller by that amount - windowInsets?.let { insets -> + activity.window.decorView.getWindowInsets()?.let { insets -> if (activity.settings().shouldUseBottomToolbar) { toolbarLocation.top -= (insets.mandatorySystemGestureInsets.bottom - insets.stableInsetBottom) } diff --git a/app/src/main/java/org/mozilla/fenix/ext/View.kt b/app/src/main/java/org/mozilla/fenix/ext/View.kt index 3e414323c..05c2e8de7 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/View.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/View.kt @@ -5,8 +5,12 @@ package org.mozilla.fenix.ext import android.graphics.Rect +import android.os.Build import android.view.TouchDelegate import android.view.View +import androidx.annotation.Dimension +import androidx.annotation.VisibleForTesting +import androidx.core.view.WindowInsetsCompat import mozilla.components.support.ktx.android.util.dpToPx fun View.increaseTapArea(extraDps: Int) { @@ -26,3 +30,61 @@ fun View.removeTouchDelegate() { parent.touchDelegate = null } } + +/** + * A safer version of [ViewCompat.getRootWindowInsets] that does not throw a NullPointerException + * if the view is not attached. + */ +fun View.getWindowInsets(): WindowInsetsCompat? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + rootWindowInsets?.let { + WindowInsetsCompat.toWindowInsetsCompat(it) + } + } else { + null + } +} + +/** + * Checks if the keyboard is visible + * + * Inspired by https://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android + * API 30 adds a native method for this. We should use it (and a compat method if one + * is added) when it becomes available + */ +fun View.isKeyboardVisible(): Boolean { + // Since we have insets in M and above, we don't need to guess what the keyboard height is. + // Otherwise, we make a guess at the minimum height of the keyboard to account for the + // navigation bar. + val minimumKeyboardHeight = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + 0 + } else { + MINIMUM_KEYBOARD_HEIGHT.dpToPx(resources.displayMetrics) + } + return getKeyboardHeight() > minimumKeyboardHeight +} + +@VisibleForTesting +internal fun View.getWindowVisibleDisplayFrame(): Rect = with(Rect()) { + getWindowVisibleDisplayFrame(this) + this +} + +@VisibleForTesting +internal fun View.getKeyboardHeight(): Int { + val windowRect = getWindowVisibleDisplayFrame() + val statusBarHeight = windowRect.top + var keyboardHeight = rootView.height - (windowRect.height() + statusBarHeight) + getWindowInsets()?.let { + keyboardHeight -= it.stableInsetBottom + } + + return keyboardHeight +} + +/** + * The assumed minimum height of the keyboard. + */ +@VisibleForTesting +@Dimension(unit = Dimension.DP) +internal const val MINIMUM_KEYBOARD_HEIGHT = 100 diff --git a/app/src/test/java/org/mozilla/fenix/ext/ViewTest.kt b/app/src/test/java/org/mozilla/fenix/ext/ViewTest.kt index a70ec0abd..c9da170fb 100644 --- a/app/src/test/java/org/mozilla/fenix/ext/ViewTest.kt +++ b/app/src/test/java/org/mozilla/fenix/ext/ViewTest.kt @@ -5,14 +5,18 @@ package org.mozilla.fenix.ext import android.graphics.Rect +import android.os.Build import android.util.DisplayMetrics import android.view.View +import android.view.WindowInsets import android.widget.FrameLayout +import androidx.core.view.WindowInsetsCompat import io.mockk.MockKAnnotations import io.mockk.Runs import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just +import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.slot import io.mockk.verify @@ -20,7 +24,11 @@ import mozilla.components.support.ktx.android.util.dpToPx import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.robolectric.annotation.Config +@RunWith(FenixRobolectricTestRunner::class) class ViewTest { @MockK private lateinit var view: View @@ -31,6 +39,7 @@ class ViewTest { fun setup() { MockKAnnotations.init(this) mockkStatic("mozilla.components.support.ktx.android.util.DisplayMetricsKt") + mockkStatic("org.mozilla.fenix.ext.ViewKt") every { view.resources.displayMetrics } returns displayMetrics every { view.parent } returns parent @@ -66,4 +75,77 @@ class ViewTest { view.removeTouchDelegate() verify { parent.touchDelegate = null } } + + @Config(sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1]) + @Test + fun `getWindowInsets returns null below API 23`() { + assertEquals(null, view.getWindowInsets()) + } + + @Test + fun `getWindowInsets returns null when the system insets don't exist`() { + every { view.rootWindowInsets } returns null + assertEquals(null, view.getWindowInsets()) + } + + @Test + fun `getWindowInsets returns the compat insets when the system insets exist`() { + val rootInsets: WindowInsets = mockk(relaxed = true) + every { view.rootWindowInsets } returns rootInsets + + assertEquals(WindowInsetsCompat.toWindowInsetsCompat(rootInsets), view.getWindowInsets()) + } + + @Config(sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1]) + @Test + fun `getKeyboardHeight accounts for status bar below API 23`() { + every { view.getWindowVisibleDisplayFrame() } returns Rect(0, 50, 1000, 500) + every { view.rootView.height } returns 1000 + + assertEquals(500, view.getKeyboardHeight()) + } + + @Test + fun `getKeyboardHeight accounts for status bar and navigation bar`() { + every { view.getWindowVisibleDisplayFrame() } returns Rect(0, 50, 1000, 500) + every { view.rootView.height } returns 1000 + every { view.getWindowInsets() } returns mockk(relaxed = true) { + every { stableInsetBottom } returns 50 + } + + assertEquals(450, view.getKeyboardHeight()) + } + + @Config(sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1]) + @Test + fun `isKeyboardVisible returns false when the keyboard height is less than or equal to the minimum threshold`() { + val threshold = MINIMUM_KEYBOARD_HEIGHT.dpToPx(displayMetrics) + + every { view.getKeyboardHeight() } returns threshold - 1 + assertEquals(false, view.isKeyboardVisible()) + + every { view.getKeyboardHeight() } returns threshold + assertEquals(false, view.isKeyboardVisible()) + } + + @Config(sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1]) + @Test + fun `isKeyboardVisible returns true when the keyboard height is greater than the minimum threshold`() { + val threshold = MINIMUM_KEYBOARD_HEIGHT.dpToPx(displayMetrics) + every { view.getKeyboardHeight() } returns threshold + 1 + + assertEquals(true, view.isKeyboardVisible()) + } + + @Test + fun `isKeyboardVisible returns false when the keyboard height is 0`() { + every { view.getKeyboardHeight() } returns 0 + assertEquals(false, view.isKeyboardVisible()) + } + + @Test + fun `isKeyboardVisible returns true when the keyboard height is greater than 0`() { + every { view.getKeyboardHeight() } returns 100 + assertEquals(true, view.isKeyboardVisible()) + } } From f75be41d3a25d7341b63aaf9670eb7bdc213ee37 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Tue, 28 Jul 2020 13:44:28 -0700 Subject: [PATCH 22/74] Add metric tests --- .../fenix/components/metrics/Metrics.kt | 18 +- .../metrics/BreadcrumbRecorderTest.kt | 22 ++- .../metrics/MetricControllerTest.kt | 170 ++++++++++++++++++ 3 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 app/src/test/java/org/mozilla/fenix/components/metrics/MetricControllerTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt index f6543991a..c914dc24e 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.components.metrics import android.content.Context +import androidx.annotation.VisibleForTesting import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts import mozilla.components.browser.errorpages.ErrorType import mozilla.components.browser.menu.facts.BrowserMenuFacts @@ -640,21 +641,26 @@ interface MetricController { } } -private class DebugMetricController : MetricController { +@VisibleForTesting +internal class DebugMetricController( + private val logger: Logger = Logger() +) : MetricController { + override fun start(type: MetricServiceType) { - Logger.debug("DebugMetricController: start") + logger.debug("DebugMetricController: start") } override fun stop(type: MetricServiceType) { - Logger.debug("DebugMetricController: stop") + logger.debug("DebugMetricController: stop") } override fun track(event: Event) { - Logger.debug("DebugMetricController: track event: $event") + logger.debug("DebugMetricController: track event: $event") } } -private class ReleaseMetricController( +@VisibleForTesting +internal class ReleaseMetricController( private val services: List, private val isDataTelemetryEnabled: () -> Boolean, private val isMarketingDataTelemetryEnabled: () -> Boolean @@ -706,7 +712,7 @@ private class ReleaseMetricController( val isEnabled = isTelemetryEnabled(it.type) val isInitialized = isInitialized(it.type) if (!isEnabled || !isInitialized) { - return + return@forEach } it.track(event) diff --git a/app/src/test/java/org/mozilla/fenix/components/metrics/BreadcrumbRecorderTest.kt b/app/src/test/java/org/mozilla/fenix/components/metrics/BreadcrumbRecorderTest.kt index 61a62981b..454cd8881 100644 --- a/app/src/test/java/org/mozilla/fenix/components/metrics/BreadcrumbRecorderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/metrics/BreadcrumbRecorderTest.kt @@ -4,8 +4,11 @@ package org.mozilla.fenix.components.metrics +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleRegistry import androidx.navigation.NavController import androidx.navigation.NavDestination +import io.mockk.Called import io.mockk.mockk import io.mockk.spyk import io.mockk.verify @@ -16,7 +19,24 @@ import mozilla.components.support.base.crash.Breadcrumb import org.junit.Assert.assertEquals import org.junit.Test -internal class BreadcrumbRecorderTest { +class BreadcrumbRecorderTest { + + @Test + fun `sets listener on create and destroy`() { + val navController: NavController = mockk(relaxUnitFun = true) + + val lifecycle = LifecycleRegistry(mockk()) + val breadCrumbRecorder = BreadcrumbsRecorder(mockk(), navController) { "test" } + + lifecycle.addObserver(breadCrumbRecorder) + verify { navController wasNot Called } + + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + verify { navController.addOnDestinationChangedListener(breadCrumbRecorder) } + + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + verify { navController.removeOnDestinationChangedListener(breadCrumbRecorder) } + } @Test fun `ensure crash reporter recordCrashBreadcrumb is called`() { diff --git a/app/src/test/java/org/mozilla/fenix/components/metrics/MetricControllerTest.kt b/app/src/test/java/org/mozilla/fenix/components/metrics/MetricControllerTest.kt new file mode 100644 index 000000000..97934c541 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/components/metrics/MetricControllerTest.kt @@ -0,0 +1,170 @@ +/* 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 io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import io.mockk.verify +import io.mockk.verifyAll +import mozilla.components.support.base.log.logger.Logger +import org.junit.Before +import org.junit.Test + +class MetricControllerTest { + + @MockK(relaxUnitFun = true) private lateinit var dataService1: MetricsService + @MockK(relaxUnitFun = true) private lateinit var dataService2: MetricsService + @MockK(relaxUnitFun = true) private lateinit var marketingService1: MetricsService + @MockK(relaxUnitFun = true) private lateinit var marketingService2: MetricsService + + @Before + fun setup() { + MockKAnnotations.init(this) + + every { dataService1.type } returns MetricServiceType.Data + every { dataService2.type } returns MetricServiceType.Data + every { marketingService1.type } returns MetricServiceType.Marketing + every { marketingService2.type } returns MetricServiceType.Marketing + } + + @Test + fun `debug metric controller emits logs`() { + val logger = mockk(relaxed = true) + val controller = DebugMetricController(logger) + + controller.start(MetricServiceType.Data) + verify { logger.debug("DebugMetricController: start") } + + controller.stop(MetricServiceType.Data) + verify { logger.debug("DebugMetricController: stop") } + + controller.track(Event.OpenedAppFirstRun) + verify { logger.debug("DebugMetricController: track event: ${Event.OpenedAppFirstRun}") } + } + + @Test + fun `release metric controller starts and stops all data services`() { + var enabled = true + val controller = ReleaseMetricController( + services = listOf(dataService1, marketingService1, dataService2, marketingService2), + isDataTelemetryEnabled = { enabled }, + isMarketingDataTelemetryEnabled = { enabled } + ) + + controller.start(MetricServiceType.Data) + verify { dataService1.start() } + verify { dataService2.start() } + + enabled = false + + controller.stop(MetricServiceType.Data) + verify { dataService1.stop() } + verify { dataService2.stop() } + + verifyAll(inverse = true) { + marketingService1.start() + marketingService1.stop() + marketingService2.start() + marketingService2.stop() + } + } + + @Test + fun `release metric controller starts data service only if enabled`() { + val controller = ReleaseMetricController( + services = listOf(dataService1), + isDataTelemetryEnabled = { false }, + isMarketingDataTelemetryEnabled = { true } + ) + + controller.start(MetricServiceType.Data) + verify(inverse = true) { dataService1.start() } + + controller.stop(MetricServiceType.Data) + verify(inverse = true) { dataService1.stop() } + } + + @Test + fun `release metric controller starts service only once`() { + var enabled = true + val controller = ReleaseMetricController( + services = listOf(dataService1), + isDataTelemetryEnabled = { enabled }, + isMarketingDataTelemetryEnabled = { true } + ) + + controller.start(MetricServiceType.Data) + controller.start(MetricServiceType.Data) + verify(exactly = 1) { dataService1.start() } + + enabled = false + + controller.stop(MetricServiceType.Data) + controller.stop(MetricServiceType.Data) + verify(exactly = 1) { dataService1.stop() } + } + + @Test + fun `release metric controller starts and stops all marketing services`() { + var enabled = true + val controller = ReleaseMetricController( + services = listOf(dataService1, marketingService1, dataService2, marketingService2), + isDataTelemetryEnabled = { enabled }, + isMarketingDataTelemetryEnabled = { enabled } + ) + + controller.start(MetricServiceType.Marketing) + verify { marketingService1.start() } + verify { marketingService2.start() } + + enabled = false + + controller.stop(MetricServiceType.Marketing) + verify { marketingService1.stop() } + verify { marketingService2.stop() } + + verifyAll(inverse = true) { + dataService1.start() + dataService1.stop() + dataService2.start() + dataService2.stop() + } + } + + @Test + fun `tracking events should be sent to matching service`() { + val controller = ReleaseMetricController( + listOf(dataService1, marketingService1), + isDataTelemetryEnabled = { true }, + isMarketingDataTelemetryEnabled = { true } + ) + every { dataService1.shouldTrack(Event.TabMediaPause) } returns false + every { marketingService1.shouldTrack(Event.TabMediaPause) } returns true + + controller.start(MetricServiceType.Marketing) + controller.track(Event.TabMediaPause) + verify { marketingService1.track(Event.TabMediaPause) } + } + + @Test + fun `tracking events should be sent to enabled service`() { + var enabled = true + val controller = ReleaseMetricController( + listOf(dataService1, marketingService1), + isDataTelemetryEnabled = { enabled }, + isMarketingDataTelemetryEnabled = { true } + ) + every { dataService1.shouldTrack(Event.TabMediaPause) } returns true + every { marketingService1.shouldTrack(Event.TabMediaPause) } returns true + + controller.start(MetricServiceType.Marketing) + enabled = false + + controller.track(Event.TabMediaPause) + verify { marketingService1.track(Event.TabMediaPause) } + } +} From 12b95b490235f1969ca4f1b4212aea5cb21dd0f3 Mon Sep 17 00:00:00 2001 From: Jeff Boek Date: Tue, 28 Jul 2020 14:18:48 -0700 Subject: [PATCH 23/74] For #11579 - Adds telemetry for autoplay settings in Site Permissions --- app/metrics.yaml | 29 ++++++++++++++++ .../components/metrics/GleanMetricsService.kt | 33 ++++++++++++------- .../fenix/components/metrics/Metrics.kt | 12 +++++++ .../SitePermissionsFragment.kt | 7 ++++ ...tePermissionsManagePhoneFeatureFragment.kt | 17 ++++++++-- docs/metrics.md | 2 ++ 6 files changed, 86 insertions(+), 14 deletions(-) diff --git a/app/metrics.yaml b/app/metrics.yaml index 77b359729..37a9de4f1 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -3207,3 +3207,32 @@ perf.awesomebar: - fenix-core@mozilla.com - gkruglov@mozilla.com expires: "2020-10-01" + +autoplay: + visited_setting: + type: event + description: A user visited the autoplay settings screen + bugs: + - https://github.com/mozilla-mobile/fenix/issues/11579 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/13041#issuecomment-665777411 + notification_emails: + - fenix-core@mozilla.com + expires: "2021-02-01" + setting_changed: + type: event + description: | + A user changed their autoplay setting to either block_cellular, + block_audio, or block_all. + extra_keys: + autoplay_setting: + description: | + The new setting for autoplay: block_cellular, + block_audio, or block_all. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/11579 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/13041#issuecomment-665777411 + notification_emails: + - fenix-core@mozilla.com + expires: "2021-02-01" 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 55ebab4f2..fb293c595 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 @@ -12,6 +12,7 @@ import mozilla.components.support.base.log.logger.Logger import org.mozilla.fenix.GleanMetrics.AboutPage import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.GleanMetrics.AppTheme +import org.mozilla.fenix.GleanMetrics.Autoplay import org.mozilla.fenix.GleanMetrics.BookmarksManagement import org.mozilla.fenix.GleanMetrics.BrowserSearch import org.mozilla.fenix.GleanMetrics.Collections @@ -45,6 +46,7 @@ import org.mozilla.fenix.GleanMetrics.SearchWidgetCfr import org.mozilla.fenix.GleanMetrics.SyncAccount import org.mozilla.fenix.GleanMetrics.SyncAuth import org.mozilla.fenix.GleanMetrics.Tab +import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.GleanMetrics.Tip import org.mozilla.fenix.GleanMetrics.ToolbarSettings import org.mozilla.fenix.GleanMetrics.TopSites @@ -633,40 +635,47 @@ private val Event.wrapper: EventWrapper<*>? ) is Event.TabsTrayOpened -> EventWrapper( - { org.mozilla.fenix.GleanMetrics.TabsTray.opened.record(it) } + { TabsTray.opened.record(it) } ) is Event.TabsTrayClosed -> EventWrapper( - { org.mozilla.fenix.GleanMetrics.TabsTray.closed.record(it) } + { TabsTray.closed.record(it) } ) is Event.OpenedExistingTab -> EventWrapper( - { org.mozilla.fenix.GleanMetrics.TabsTray.openedExistingTab.record(it) } + { TabsTray.openedExistingTab.record(it) } ) is Event.ClosedExistingTab -> EventWrapper( - { org.mozilla.fenix.GleanMetrics.TabsTray.closedExistingTab.record(it) } + { TabsTray.closedExistingTab.record(it) } ) is Event.TabsTrayPrivateModeTapped -> EventWrapper( - { org.mozilla.fenix.GleanMetrics.TabsTray.privateModeTapped.record(it) } + { TabsTray.privateModeTapped.record(it) } ) is Event.TabsTrayNormalModeTapped -> EventWrapper( - { org.mozilla.fenix.GleanMetrics.TabsTray.normalModeTapped.record(it) } + { TabsTray.normalModeTapped.record(it) } ) is Event.NewTabTapped -> EventWrapper( - { org.mozilla.fenix.GleanMetrics.TabsTray.newTabTapped.record(it) } + { TabsTray.newTabTapped.record(it) } ) is Event.NewPrivateTabTapped -> EventWrapper( - { org.mozilla.fenix.GleanMetrics.TabsTray.newPrivateTabTapped.record(it) } + { TabsTray.newPrivateTabTapped.record(it) } ) is Event.TabsTrayMenuOpened -> EventWrapper( - { org.mozilla.fenix.GleanMetrics.TabsTray.menuOpened.record(it) } + { TabsTray.menuOpened.record(it) } ) is Event.TabsTraySaveToCollectionPressed -> EventWrapper( - { org.mozilla.fenix.GleanMetrics.TabsTray.saveToCollection.record(it) } + { TabsTray.saveToCollection.record(it) } ) is Event.TabsTrayShareAllTabsPressed -> EventWrapper( - { org.mozilla.fenix.GleanMetrics.TabsTray.shareAllTabs.record(it) } + { TabsTray.shareAllTabs.record(it) } ) is Event.TabsTrayCloseAllTabsPressed -> EventWrapper( - { org.mozilla.fenix.GleanMetrics.TabsTray.closeAllTabs.record(it) } + { TabsTray.closeAllTabs.record(it) } + ) + Event.AutoPlaySettingVisited -> EventWrapper( + { Autoplay.visitedSetting.record(it) } + ) + is Event.AutoPlaySettingChanged -> EventWrapper( + { Autoplay.settingChanged.record(it) }, + { Autoplay.settingChangedKeys.valueOf(it) } ) // Don't record other events in Glean: diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt index c914dc24e..93ce11cc7 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt @@ -33,6 +33,7 @@ import mozilla.components.support.webextensions.facts.WebExtensionFacts import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.GleanMetrics.AppTheme +import org.mozilla.fenix.GleanMetrics.Autoplay import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.GleanMetrics.ContextMenu import org.mozilla.fenix.GleanMetrics.CrashReporter @@ -514,6 +515,17 @@ sealed class Event { get() = mapOf(Events.tabCounterMenuActionKeys.item to item.toString().toLowerCase(Locale.ROOT)) } + object AutoPlaySettingVisited : Event() + + data class AutoPlaySettingChanged(val setting: AutoplaySetting) : Event() { + enum class AutoplaySetting { + BLOCK_CELLULAR, BLOCK_AUDIO, BLOCK_ALL + } + + override val extras: Map? + get() = mapOf(Autoplay.settingChangedKeys.autoplaySetting to setting.toString().toLowerCase(Locale.ROOT)) + } + sealed class Search internal open val extras: Map<*, String>? diff --git a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsFragment.kt index 2867f8a62..a87f1041e 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsFragment.kt @@ -10,7 +10,9 @@ import androidx.preference.Preference import androidx.preference.Preference.OnPreferenceClickListener import androidx.preference.PreferenceFragmentCompat import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.getPreferenceKey +import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.settings.PhoneFeature @@ -78,6 +80,11 @@ class SitePermissionsFragment : PreferenceFragmentCompat() { private fun navigateToPhoneFeature(phoneFeature: PhoneFeature) { val directions = SitePermissionsFragmentDirections .actionSitePermissionsToManagePhoneFeatures(phoneFeature) + + if (phoneFeature == PhoneFeature.AUTOPLAY_AUDIBLE) { + requireComponents.analytics.metrics.track(Event.AutoPlaySettingVisited) + } + Navigation.findNavController(requireView()).navigate(directions) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManagePhoneFeatureFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManagePhoneFeatureFragment.kt index 11b0a4f4d..d96f44836 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManagePhoneFeatureFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManagePhoneFeatureFragment.kt @@ -27,6 +27,8 @@ import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.AL import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.ASK_TO_ALLOW import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.BLOCKED import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_AUDIBLE @@ -180,16 +182,27 @@ class SitePermissionsManagePhoneFeatureFragment : Fragment() { */ private fun saveActionInSettings(autoplaySetting: Int) { settings.setAutoplayUserSetting(autoplaySetting) + val setting: Event.AutoPlaySettingChanged.AutoplaySetting + val (audible, inaudible) = when (autoplaySetting) { AUTOPLAY_ALLOW_ALL, AUTOPLAY_ALLOW_ON_WIFI -> { settings.setAutoplayUserSetting(AUTOPLAY_ALLOW_ON_WIFI) + setting = Event.AutoPlaySettingChanged.AutoplaySetting.BLOCK_CELLULAR + BLOCKED to BLOCKED + } + AUTOPLAY_BLOCK_AUDIBLE -> { + setting = Event.AutoPlaySettingChanged.AutoplaySetting.BLOCK_AUDIO + BLOCKED to ALLOWED + } + AUTOPLAY_BLOCK_ALL -> { + setting = Event.AutoPlaySettingChanged.AutoplaySetting.BLOCK_ALL BLOCKED to BLOCKED } - AUTOPLAY_BLOCK_AUDIBLE -> BLOCKED to ALLOWED - AUTOPLAY_BLOCK_ALL -> BLOCKED to BLOCKED else -> return } + + requireComponents.analytics.metrics.track(Event.AutoPlaySettingChanged(setting)) settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, audible) settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, inaudible) } diff --git a/docs/metrics.md b/docs/metrics.md index 0ea774ade..c9482a603 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -56,6 +56,8 @@ The following metrics are added to the ping: | addons.open_addon_in_toolbar_menu |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user interacted with an installed add-on in the toolbar menu |[1](https://github.com/mozilla-mobile/fenix/pull/8318)|
  • addon_id: The id of the add-on that was interacted with in the toolbar menu
|2020-10-01 | | | addons.open_addons_in_settings |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user accessed "Add-ons" from the Settings |[1](https://github.com/mozilla-mobile/fenix/pull/8318)||2020-10-01 | | | app_theme.dark_theme_selected |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user selected Dark Theme |[1](https://github.com/mozilla-mobile/fenix/pull/7968)|
  • source: The source from where dark theme was selected. The source can be 'SETTINGS' or 'ONBOARDING'
|2020-10-01 | | +| autoplay.setting_changed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user changed their autoplay setting to either block_cellular, block_audio, or block_all. |[1](https://github.com/mozilla-mobile/fenix/pull/13041#issuecomment-665777411)|
  • autoplay_setting: The new setting for autoplay: block_cellular, block_audio, or block_all.
|2021-02-01 | | +| autoplay.visited_setting |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user visited the autoplay settings screen |[1](https://github.com/mozilla-mobile/fenix/pull/13041#issuecomment-665777411)||2021-02-01 | | | bookmarks_management.copied |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user copied a bookmark. |[1](https://github.com/mozilla-mobile/fenix/pull/1708)||2020-10-01 | | | bookmarks_management.edited |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user edited the title and/or URL of an existing bookmark. |[1](https://github.com/mozilla-mobile/fenix/pull/1708)||2020-10-01 | | | bookmarks_management.folder_add |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user added a new bookmark folder. |[1](https://github.com/mozilla-mobile/fenix/pull/1708)||2020-10-01 | | From a72f9c75f014f318b342392d497b5882cc668432 Mon Sep 17 00:00:00 2001 From: Jeff Boek Date: Wed, 29 Jul 2020 15:26:57 -0700 Subject: [PATCH 24/74] For #12570 - Correctly removes search suggestions when search query and url is empty --- .../java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt index 0a0382936..7d918a1e0 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt @@ -170,7 +170,7 @@ class AwesomeBarView( updateSuggestionProvidersVisibility(state) // Do not make suggestions based on user's current URL unless it's a search shortcut - if (state.query == state.url && !state.showSearchShortcuts) { + if (state.query.isNotEmpty() && state.query == state.url && !state.showSearchShortcuts) { return } From 0e3acfcd9467060c9ae056a715c777516c7d12e8 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Wed, 29 Jul 2020 15:59:16 -0700 Subject: [PATCH 25/74] Move config to robolectric.properties --- .../helpers/FenixRobolectricTestRunner.kt | 19 +++++++------------ app/src/test/resources/robolectric.properties | 3 ++- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app/src/test/java/org/mozilla/fenix/helpers/FenixRobolectricTestRunner.kt b/app/src/test/java/org/mozilla/fenix/helpers/FenixRobolectricTestRunner.kt index 76d1b8e9a..37f5f4757 100644 --- a/app/src/test/java/org/mozilla/fenix/helpers/FenixRobolectricTestRunner.kt +++ b/app/src/test/java/org/mozilla/fenix/helpers/FenixRobolectricTestRunner.kt @@ -5,12 +5,12 @@ package org.mozilla.fenix.helpers import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config /** - * A test runner that starts Robolectric with our custom configuration for use in unit tests. This - * should ALWAYS be used instead of RobolectricTestRunner and AndroidJUnit4. You should only use - * Robolectric when necessary because it non-trivially increases test duration. + * A test runner that was added to start Robolectric with our custom configuration for use in unit tests. + * + * This class is now deprecated as the configuration is set by robolectric.properties instead. + * You should only use Robolectric when necessary because it non-trivially increases test duration. * * usage: * ``` @@ -29,12 +29,7 @@ import org.robolectric.annotation.Config * We chose the name RobolectricTestRunner because we want folks to know they're starting Robolectric * because it increases test runtime. Furthermore, the naming of 3) is unclear so we didn't want to * use that name. + * + * As a result, new tests should use RobolectricTestRunner. */ -class FenixRobolectricTestRunner(testClass: Class<*>) : RobolectricTestRunner(testClass) { - - override fun buildGlobalConfig(): Config { - return Config.Builder() - .setApplication(FenixRobolectricTestApplication::class.java) - .build() - } -} +class FenixRobolectricTestRunner(testClass: Class<*>) : RobolectricTestRunner(testClass) diff --git a/app/src/test/resources/robolectric.properties b/app/src/test/resources/robolectric.properties index 89a6c8b4c..e341e6bfb 100644 --- a/app/src/test/resources/robolectric.properties +++ b/app/src/test/resources/robolectric.properties @@ -1 +1,2 @@ -sdk=28 \ No newline at end of file +sdk=28 +application=org.mozilla.fenix.helpers.FenixRobolectricTestApplication From 151e8595b678e718648169919652a051ced49522 Mon Sep 17 00:00:00 2001 From: Kate Glazko Date: Fri, 24 Jul 2020 14:43:12 -0700 Subject: [PATCH 26/74] For AC #7673 Move DownloadStatus to DownloadState Fenix Side Changes --- .../org/mozilla/fenix/browser/BaseBrowserFragment.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 4a0bb9799..5597ef08b 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -378,8 +378,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus -> // If the download is just paused, don't show any in-app notification - if (downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.COMPLETED || - downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.FAILED + if (downloadJobStatus == DownloadState.Status.COMPLETED || + downloadJobStatus == DownloadState.Status.FAILED ) { saveDownloadDialogState( @@ -391,7 +391,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session val dynamicDownloadDialog = DynamicDownloadDialog( container = view.browserLayout, downloadState = downloadState, - didFail = downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.FAILED, + didFail = downloadJobStatus == DownloadState.Status.FAILED, tryAgain = downloadFeature::tryAgain, onCannotOpenFile = { FenixSnackbar.make( @@ -616,12 +616,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session private fun saveDownloadDialogState( sessionId: String?, downloadState: DownloadState, - downloadJobStatus: AbstractFetchDownloadService.DownloadJobStatus + downloadJobStatus: DownloadState.Status ) { sessionId?.let { id -> sharedViewModel.downloadDialogState[id] = Pair( downloadState, - downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.FAILED + downloadJobStatus == DownloadState.Status.FAILED ) } } From 611adf83e5476cb64c6926a78d8757b78c0aa27c Mon Sep 17 00:00:00 2001 From: Kate Glazko Date: Fri, 24 Jul 2020 14:57:48 -0700 Subject: [PATCH 27/74] remove unused import --- .../main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt | 1 - 1 file changed, 1 deletion(-) 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 5597ef08b..54f0ccb2d 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -48,7 +48,6 @@ import mozilla.components.feature.accounts.FxaWebChannelFeature import mozilla.components.feature.app.links.AppLinksFeature import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.contextmenu.ContextMenuFeature -import mozilla.components.feature.downloads.AbstractFetchDownloadService import mozilla.components.feature.downloads.DownloadsFeature import mozilla.components.feature.downloads.manager.FetchDownloadManager import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID From aaae70f3dc9db8e58b70a4220bc84c513a64750f Mon Sep 17 00:00:00 2001 From: Jeff Boek Date: Wed, 29 Jul 2020 15:30:18 -0700 Subject: [PATCH 28/74] No Issue - Updates AC --- buildSrc/src/main/java/AndroidComponents.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/AndroidComponents.kt b/buildSrc/src/main/java/AndroidComponents.kt index a7e27b58b..ca756537a 100644 --- a/buildSrc/src/main/java/AndroidComponents.kt +++ b/buildSrc/src/main/java/AndroidComponents.kt @@ -3,5 +3,5 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ object AndroidComponents { - const val VERSION = "52.0.20200728130711" + const val VERSION = "53.0.20200729190533" } From 1a51b7f87471c1065cfcc736c1292a7cd4c456ed Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Wed, 29 Jul 2020 16:48:03 -0700 Subject: [PATCH 29/74] For #13084 - Use runBlockingTest --- .../controller/SavedLoginsStorageController.kt | 14 ++++++++------ .../logins/SavedLoginsStorageControllerTest.kt | 15 +++++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) 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 e04e1418a..754ddb688 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 @@ -7,6 +7,7 @@ package org.mozilla.fenix.settings.logins.controller import android.util.Log import androidx.navigation.NavController import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers @@ -31,14 +32,15 @@ open class SavedLoginsStorageController( private val passwordsStorage: SyncableLoginsStorage, private val viewLifecycleScope: CoroutineScope, private val navController: NavController, - private val loginsFragmentStore: LoginsFragmentStore + private val loginsFragmentStore: LoginsFragmentStore, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO ) { private suspend fun getLogin(loginId: String): Login? = passwordsStorage.get(loginId) fun delete(loginId: String) { var deleteLoginJob: Deferred? = null - val deleteJob = viewLifecycleScope.launch(Dispatchers.IO) { + val deleteJob = viewLifecycleScope.launch(ioDispatcher) { deleteLoginJob = async { passwordsStorage.delete(loginId) } @@ -56,7 +58,7 @@ open class SavedLoginsStorageController( fun save(loginId: String, usernameText: String, passwordText: String) { var saveLoginJob: Deferred? = null - viewLifecycleScope.launch(Dispatchers.IO) { + viewLifecycleScope.launch(ioDispatcher) { saveLoginJob = async { // must retrieve from storage to get the httpsRealm and formActionOrigin val oldLogin = passwordsStorage.get(loginId) @@ -123,7 +125,7 @@ open class SavedLoginsStorageController( fun findPotentialDuplicates(loginId: String) { var deferredLogin: Deferred>? = null // What scope should be used here? - val fetchLoginJob = viewLifecycleScope.launch(Dispatchers.IO) { + val fetchLoginJob = viewLifecycleScope.launch(ioDispatcher) { deferredLogin = async { val login = getLogin(loginId) passwordsStorage.getPotentialDupesIgnoringUsername(login!!) @@ -149,7 +151,7 @@ open class SavedLoginsStorageController( fun fetchLoginDetails(loginId: String) { var deferredLogin: Deferred>? = null - val fetchLoginJob = viewLifecycleScope.launch(Dispatchers.IO) { + val fetchLoginJob = viewLifecycleScope.launch(ioDispatcher) { deferredLogin = async { passwordsStorage.list() } @@ -177,7 +179,7 @@ open class SavedLoginsStorageController( fun handleLoadAndMapLogins() { var deferredLogins: Deferred>? = null - val fetchLoginsJob = viewLifecycleScope.launch(Dispatchers.IO) { + val fetchLoginsJob = viewLifecycleScope.launch(ioDispatcher) { deferredLogins = async { passwordsStorage.list() } diff --git a/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsStorageControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsStorageControllerTest.kt index f9cb599dc..d29acb3ed 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsStorageControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsStorageControllerTest.kt @@ -13,9 +13,9 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.runBlockingTest import mozilla.components.concept.storage.Login import mozilla.components.service.sync.logins.SyncableLoginsStorage import mozilla.components.support.test.rule.MainCoroutineRule @@ -41,6 +41,7 @@ class SavedLoginsStorageControllerTest { private val navController: NavController = mockk(relaxed = true) private val loginsFragmentStore: LoginsFragmentStore = mockk(relaxed = true) private val scope = TestCoroutineScope() + private val ioDispatcher = TestCoroutineDispatcher() private val loginMock: Login = mockk(relaxed = true) @Before @@ -55,17 +56,19 @@ class SavedLoginsStorageControllerTest { passwordsStorage = passwordsStorage, viewLifecycleScope = scope, navController = navController, - loginsFragmentStore = loginsFragmentStore + loginsFragmentStore = loginsFragmentStore, + ioDispatcher = ioDispatcher ) } @After fun cleanUp() { scope.cleanupTestCoroutines() + ioDispatcher.cleanupTestCoroutines() } @Test - fun `WHEN a login is deleted, THEN navigate back to the previous page`() = runBlocking { + fun `WHEN a login is deleted, THEN navigate back to the previous page`() = scope.runBlockingTest { val loginId = "id" coEvery { passwordsStorage.delete(any()) } returns true controller.delete(loginId) @@ -77,7 +80,7 @@ class SavedLoginsStorageControllerTest { } @Test - fun `WHEN fetching the login list, THEN update the state in the store`() { + fun `WHEN fetching the login list, THEN update the state in the store`() = scope.runBlockingTest { val login = Login( guid = "id", origin = "https://www.test.co.gov.org", @@ -103,7 +106,7 @@ class SavedLoginsStorageControllerTest { } @Test - fun `WHEN saving an update to an item, THEN navigate to login detail view`() { + fun `WHEN saving an update to an item, THEN navigate to login detail view`() = scope.runBlockingTest { val oldLogin = Login( guid = "id", origin = "https://www.test.co.gov.org", @@ -147,7 +150,7 @@ class SavedLoginsStorageControllerTest { } @Test - fun `WHEN finding login dupes, THEN update duplicates in the store`() { + fun `WHEN finding login dupes, THEN update duplicates in the store`() = scope.runBlockingTest { val login = Login( guid = "id", origin = "https://www.test.co.gov.org", From ef4e8651054a74ac9bf3178df421fd68a5b9bf33 Mon Sep 17 00:00:00 2001 From: Mozilla L10n Automation Bot Date: Thu, 30 Jul 2020 00:14:20 +0000 Subject: [PATCH 30/74] Import l10n. --- app/src/main/res/values-es-rCL/strings.xml | 26 +++++++++++++++++++++ app/src/main/res/values-hr/strings.xml | 27 ++++++++++++++++++++++ app/src/main/res/values-ko/strings.xml | 6 ++--- app/src/main/res/values-ro/strings.xml | 6 ++--- 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-es-rCL/strings.xml b/app/src/main/res/values-es-rCL/strings.xml index 6c6bc6ada..86efd0030 100644 --- a/app/src/main/res/values-es-rCL/strings.xml +++ b/app/src/main/res/values-es-rCL/strings.xml @@ -24,6 +24,29 @@ %1$s pestañas abiertas. Toca para cambiar de pestaña. + + %1$d seleccionadas + + Añadir nueva colección + + Nombre + + Seleccionar colección + + Salir del modo de selección múltiple + + Guardar pestañas seleccionadas en la colección + + %1$s seleccionada + + %1$s deseleccionada + + Has salido del modo de selección múltiple + + Has ingresado al modo de selección múltiple, selecciona pestañas para guardar en una colección + + Seleccionadas + %1$s es producido por Mozilla. @@ -508,6 +531,9 @@ %1$s (modo privado) + + Guardar + Eliminar historial diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 95f1bc3c0..8004e9980 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -25,6 +25,30 @@ %1$s otvorene kartice. Dodirni za prebacivanje kartica. + + Odabrano: %1$d + + Dodaj novu zbirku + + Ime + + Odaberi zbirku + + Izađi iz modusa višestrukog odabira + + Spremi odabrane kartice u zbirku + + Odabrano: %1$s + + Nedabrano: %1$s + + Izađeno je iz modusa višestrukog odabira + + + Modus višestrukog izbora pokrenut, odaberi kartice za spremanje u zbirku + + Odabrano + %1$s proizvodi Mozilla. @@ -511,6 +535,9 @@ %1$s (privatni modus) + + Spremi + Izbriši povijest diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 6997a10e0..fd78aa446 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -289,7 +289,7 @@ 데이터 수집 - 개인 정보 정책 + 개인정보 보호정책 개발자 도구 @@ -1083,7 +1083,7 @@      - 개인 정보 보호 정책 읽기 + 개인정보 보호정책 읽기 닫기 @@ -1237,7 +1237,7 @@ 충돌 - 개인 정보 보호 정책 + 개인정보 보호정책 권리 읽기 diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 45557d742..98ecc9984 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -780,7 +780,7 @@ Folosite recent - Autentificare în Sync + Autentifică-te în Sync Trimite pe toate dispozitivele @@ -984,7 +984,7 @@ Autentificare… - Autentificare în Firefox + Autentifică-te în Firefox Rămâi deconectat @@ -1244,7 +1244,7 @@ Reconectare - Autentificare în Sync + Autentifică-te în Sync Date de autentificare salvate From ed8a9bd1a01db82ab52c876170468df5dfb47d34 Mon Sep 17 00:00:00 2001 From: mcarare Date: Thu, 30 Jul 2020 12:17:13 +0300 Subject: [PATCH 31/74] For #12565: Remove activity from DefaultTabTrayController constructor. --- .../fenix/tabtray/TabTrayController.kt | 24 ++++++++++++------- .../fenix/tabtray/TabTrayDialogFragment.kt | 8 +++++-- .../tabtray/DefaultTabTrayControllerTest.kt | 19 ++++++++------- 3 files changed, 31 insertions(+), 20 deletions(-) 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 6a8958573..ad71ced1c 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt @@ -8,13 +8,15 @@ import androidx.annotation.VisibleForTesting import androidx.navigation.NavController import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.session.Session +import mozilla.components.browser.session.SessionManager +import mozilla.components.concept.engine.profiler.Profiler import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.tabstray.Tab import mozilla.components.feature.tabs.TabsUseCases import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.components.TabCollectionStorage -import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.home.HomeFragment @@ -41,7 +43,9 @@ interface TabTrayController { /** * Default behavior of [TabTrayController]. Other implementations are possible. * - * @param activity [HomeActivity] used for context and other Android interactions. + * @param profiler [Profiler] used for profiling. + * @param sessionManager [HomeActivity] used for retrieving a list of sessions. + * @param browsingModeManager [HomeActivity] used for registering browsing mode. * @param navController [NavController] used for navigation. * @param dismissTabTray callback allowing to request this entire Fragment to be dismissed. * @param tabTrayDialogFragmentStore [TabTrayDialogFragmentStore] holding the State for all Views displayed @@ -55,7 +59,10 @@ interface TabTrayController { */ @Suppress("TooManyFunctions") class DefaultTabTrayController( - private val activity: HomeActivity, + private val profiler: Profiler?, + private val sessionManager: SessionManager, + private val browsingModeManager: BrowsingModeManager, + private val tabCollectionStorage: TabCollectionStorage, private val navController: NavController, private val dismissTabTray: () -> Unit, private val dismissTabTrayAndNavigateHome: (String) -> Unit, @@ -65,14 +72,13 @@ class DefaultTabTrayController( private val showChooseCollectionDialog: (List) -> Unit, private val showAddNewCollectionDialog: (List) -> Unit ) : TabTrayController { - private val tabCollectionStorage = activity.components.core.tabCollectionStorage override fun onNewTabTapped(private: Boolean) { - val startTime = activity.components.core.engine.profiler?.getProfilerTime() - activity.browsingModeManager.mode = BrowsingMode.fromBoolean(private) + val startTime = profiler?.getProfilerTime() + browsingModeManager.mode = BrowsingMode.fromBoolean(private) navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) dismissTabTray() - activity.components.core.engine.profiler?.addMarker( + profiler?.addMarker( "DefaultTabTrayController.onNewTabTapped", startTime ) @@ -84,7 +90,7 @@ class DefaultTabTrayController( override fun onSaveToCollectionClicked(selectedTabs: Set) { val sessionList = selectedTabs.map { - activity.components.core.sessionManager.findSessionById(it.id) ?: return + sessionManager.findSessionById(it.id) ?: return } // Only register the observer right before moving to collection creation @@ -141,7 +147,7 @@ class DefaultTabTrayController( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) private fun getListOfSessions(private: Boolean): List { - return activity.components.core.sessionManager.sessionsOfType(private = private).toList() + return sessionManager.sessionsOfType(private = private).toList() } override fun onModeRequested(): TabTrayDialogFragmentState.Mode { diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt index f663fdd7c..a4417ef15 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt @@ -163,7 +163,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler @OptIn(ExperimentalCoroutinesApi::class) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate + val activity = activity as HomeActivity + val isPrivate = activity.browsingModeManager.mode.isPrivate val thumbnailLoader = ThumbnailLoader(requireContext().components.core.thumbnailStorage) val adapter = FenixTabsAdapter(requireContext(), thumbnailLoader) @@ -173,7 +174,10 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler adapter, interactor = TabTrayFragmentInteractor( DefaultTabTrayController( - activity = (activity as HomeActivity), + profiler = activity.components.core.engine.profiler, + sessionManager = activity.components.core.sessionManager, + browsingModeManager = activity.browsingModeManager, + tabCollectionStorage = activity.components.core.tabCollectionStorage, navController = findNavController(), dismissTabTray = ::dismissAllowingStateLoss, dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome, diff --git a/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt index bcf42d32d..57ce54da4 100644 --- a/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt @@ -18,6 +18,7 @@ import io.mockk.verifyOrder import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager +import mozilla.components.concept.engine.profiler.Profiler import mozilla.components.concept.tabstray.Tab import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tabs.TabsUseCases @@ -25,18 +26,18 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.components.TabCollectionStorage -import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.sessionsOfType @OptIn(ExperimentalCoroutinesApi::class) class DefaultTabTrayControllerTest { - private val activity: HomeActivity = mockk(relaxed = true) + private val profiler: Profiler? = mockk(relaxed = true) private val navController: NavController = mockk() private val sessionManager: SessionManager = mockk(relaxed = true) + private val browsingModeManager: BrowsingModeManager = mockk(relaxed = true) private val dismissTabTray: (() -> Unit) = mockk(relaxed = true) private val dismissTabTrayAndNavigateHome: ((String) -> Unit) = mockk(relaxed = true) private val registerCollectionStorageObserver: (() -> Unit) = mockk(relaxed = true) @@ -64,9 +65,6 @@ class DefaultTabTrayControllerTest { @Before fun setUp() { mockkStatic("org.mozilla.fenix.ext.SessionManagerKt") - every { activity.components.core.sessionManager } returns sessionManager - every { activity.components.core.tabCollectionStorage } returns tabCollectionStorage - every { activity.components.core.engine.profiler } returns mockk(relaxed = true) every { sessionManager.sessionsOfType(private = true) } returns listOf(session).asSequence() every { sessionManager.sessionsOfType(private = false) } returns listOf(nonPrivateSession).asSequence() @@ -83,7 +81,10 @@ class DefaultTabTrayControllerTest { every { tabCollection.title } returns "Collection title" controller = DefaultTabTrayController( - activity = activity, + profiler = profiler, + sessionManager = sessionManager, + browsingModeManager = browsingModeManager, + tabCollectionStorage = tabCollectionStorage, navController = navController, dismissTabTray = dismissTabTray, dismissTabTrayAndNavigateHome = dismissTabTrayAndNavigateHome, @@ -100,7 +101,7 @@ class DefaultTabTrayControllerTest { controller.onNewTabTapped(private = false) verifyOrder { - activity.browsingModeManager.mode = BrowsingMode.fromBoolean(false) + browsingModeManager.mode = BrowsingMode.fromBoolean(false) navController.navigate( TabTrayDialogFragmentDirections.actionGlobalHome( focusOnAddressBar = true @@ -112,7 +113,7 @@ class DefaultTabTrayControllerTest { controller.onNewTabTapped(private = true) verifyOrder { - activity.browsingModeManager.mode = BrowsingMode.fromBoolean(true) + browsingModeManager.mode = BrowsingMode.fromBoolean(true) navController.navigate( TabTrayDialogFragmentDirections.actionGlobalHome( focusOnAddressBar = true From 0356bea50ee44f5fc31479fea56b9d5aa1eddd48 Mon Sep 17 00:00:00 2001 From: ekager Date: Thu, 30 Jul 2020 09:37:47 -0400 Subject: [PATCH 32/74] For #13117 - Hide save to collection button for private tabs in tab tray --- .../fenix/tabtray/SaveToCollectionsButtonAdapter.kt | 11 +++++++++++ .../java/org/mozilla/fenix/tabtray/TabTrayView.kt | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/SaveToCollectionsButtonAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabtray/SaveToCollectionsButtonAdapter.kt index 779e8445a..896fdea7d 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/SaveToCollectionsButtonAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/SaveToCollectionsButtonAdapter.kt @@ -32,6 +32,17 @@ class SaveToCollectionsButtonAdapter( return ViewHolder(itemView, interactor) } + override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList) { + if (payloads.isNullOrEmpty()) { + onBindViewHolder(holder, position) + return + } + + (payloads[0] as TabTrayView.TabChange).let { + holder.itemView.isVisible = it == TabTrayView.TabChange.NORMAL + } + } + override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.itemView.isVisible = interactor.onModeRequested() is TabTrayDialogFragmentState.Mode.Normal 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 44b1f609d..f11930604 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt @@ -233,9 +233,21 @@ class TabTrayView( behavior.state = BottomSheetBehavior.STATE_EXPANDED } + enum class TabChange { + PRIVATE, NORMAL + } + + private fun toggleSaveToCollectionButton(isPrivate: Boolean) { + collectionsButtonAdapter.notifyItemChanged( + 0, + if (isPrivate) TabChange.PRIVATE else TabChange.NORMAL + ) + } + override fun onTabSelected(tab: TabLayout.Tab?) { toggleFabText(isPrivateModeSelected) filterTabs.invoke(isPrivateModeSelected) + toggleSaveToCollectionButton(isPrivateModeSelected) updateUINormalMode(view.context.components.core.store.state) scrollToTab(view.context.components.core.store.state.selectedTabId) From 79c3f3c925665438c5320423cb6450b7ff1b6bc4 Mon Sep 17 00:00:00 2001 From: jhugman Date: Thu, 30 Jul 2020 17:28:26 +0000 Subject: [PATCH 33/74] =?UTF-8?q?For=20#11664=20=E2=80=94=20Fixup=20Missin?= =?UTF-8?q?gResourceExceptions=20being=20thrown=20in=20exotic=20Locales=20?= =?UTF-8?q?(#13124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Our kotlin code is not catching the `MissingResourceException` in the `LeanplumMetricsService` which results in the app crashing when the locale isn't known by the device. Catches the exception, and falls back to the ISO 639 language code. This isn't a great solution, because ISO 639 isn't especially stable. In practice however this is almost certainly never going to be a problem because Leanplum isn't going to be supported in such exotic locales. In this case, using the ISO 639 language code allows the error message to be more informative. --- .../metrics/LeanplumMetricsService.kt | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt index 99a0d4a50..aa8f712c9 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt @@ -21,6 +21,7 @@ import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.components.metrics.MozillaProductDetector.MozillaProducts import org.mozilla.fenix.ext.settings import java.util.Locale +import java.util.MissingResourceException import java.util.UUID.randomUUID private val Event.name: String? @@ -83,12 +84,19 @@ class LeanplumMetricsService(private val application: Application) : MetricsServ leanplumJob = scope.launch { val applicationSetLocale = LocaleManager.getCurrentLocale(application) - val currentLocale = when (applicationSetLocale != null) { - true -> applicationSetLocale.isO3Language - false -> Locale.getDefault().isO3Language - } - if (!isLeanplumEnabled(currentLocale)) { - Log.i(LOGTAG, "Leanplum is not available for this locale: $currentLocale") + val currentLocale = applicationSetLocale ?: Locale.getDefault() + val languageCode = + currentLocale.iso3LanguageOrNull + ?: currentLocale.language.let { + if (it.isNotBlank()) { + it + } else { + currentLocale.toString() + } + } + + if (!isLeanplumEnabled(languageCode)) { + Log.i(LOGTAG, "Leanplum is not available for this locale: $languageCode") return@launch } @@ -170,6 +178,12 @@ class LeanplumMetricsService(private val application: Application) : MetricsServ return LEANPLUM_ENABLED_LOCALES.contains(locale) } + private val Locale.iso3LanguageOrNull: String? + get() = + try { + this.isO3Language + } catch (_: MissingResourceException) { null } + companion object { private const val LOGTAG = "LeanplumMetricsService" @@ -181,7 +195,7 @@ class LeanplumMetricsService(private val application: Application) : MetricsServ get() = BuildConfig.LEANPLUM_TOKEN.orEmpty() // Leanplum needs to be enabled for the following locales. // Irrespective of the actual device location. - private val LEANPLUM_ENABLED_LOCALES = listOf( + private val LEANPLUM_ENABLED_LOCALES = setOf( "eng", // English "zho", // Chinese "deu", // German From e265bd72661198eee89e69349bf34f626d033f73 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Thu, 30 Jul 2020 09:37:19 -0700 Subject: [PATCH 34/74] Use uplifted addon date code --- .../java/org/mozilla/fenix/addons/AddonDetailsView.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsView.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsView.kt index d77e5c13c..b65dfc680 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsView.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsView.kt @@ -17,10 +17,10 @@ import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.fragment_add_on_details.* import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.ui.translatedDescription +import mozilla.components.feature.addons.ui.updatedAtDate import org.mozilla.fenix.R import java.text.DateFormat import java.text.NumberFormat -import java.text.SimpleDateFormat import java.util.Locale interface AddonDetailsInteractor { @@ -44,7 +44,6 @@ class AddonDetailsView( private val interactor: AddonDetailsInteractor ) : LayoutContainer { - private val dateParser = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()) private val dateFormatter = DateFormat.getDateInstance() private val numberFormatter = NumberFormat.getNumberInstance(Locale.getDefault()) @@ -76,7 +75,7 @@ class AddonDetailsView( } private fun bindLastUpdated(addon: Addon) { - last_updated_text.text = formatDate(addon.updatedAt) + last_updated_text.text = dateFormatter.format(addon.updatedAtDate) } private fun bindVersion(addon: Addon) { @@ -132,8 +131,4 @@ class AddonDetailsView( spannableStringBuilder.setSpan(clickable, start, end, flags) spannableStringBuilder.removeSpan(link) } - - private fun formatDate(text: String): String { - return dateFormatter.format(dateParser.parse(text)!!) - } } From d8ff6179fe738c0343fd8955cd8de43c65af7366 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Thu, 30 Jul 2020 11:21:17 -0700 Subject: [PATCH 35/74] For #7992: Use favicon style from AC (#12608) --- app/build.gradle | 1 + .../topsites/TopSiteItemViewHolder.kt | 3 +- .../logins/view/LoginsListViewHolder.kt | 19 ++--- .../res/drawable/notification_indicator.xml | 2 +- .../res/drawable/quick_action_icon_close.xml | 29 ------- ...ick_action_icon_read_with_notification.xml | 21 ----- .../main/res/drawable/reader_two_state.xml | 10 --- .../reader_two_state_with_notification.xml | 10 --- .../res/drawable/top_sites_background.xml | 10 --- app/src/main/res/layout/logins_item.xml | 29 ++----- app/src/main/res/layout/top_site_item.xml | 21 +---- app/src/main/res/values-night/colors.xml | 5 +- app/src/main/res/values/colors.xml | 85 +++++++++---------- app/src/main/res/values/styles.xml | 8 ++ buildSrc/src/main/java/Dependencies.kt | 1 + 15 files changed, 74 insertions(+), 180 deletions(-) delete mode 100644 app/src/main/res/drawable/quick_action_icon_close.xml delete mode 100644 app/src/main/res/drawable/quick_action_icon_read_with_notification.xml delete mode 100644 app/src/main/res/drawable/reader_two_state.xml delete mode 100644 app/src/main/res/drawable/reader_two_state_with_notification.xml delete mode 100644 app/src/main/res/drawable/top_sites_background.xml diff --git a/app/build.gradle b/app/build.gradle index ad79842d8..0aab34e4e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -470,6 +470,7 @@ dependencies { implementation Deps.mozilla_ui_colors implementation Deps.mozilla_ui_icons implementation Deps.mozilla_ui_publicsuffixlist + implementation Deps.mozilla_ui_widgets implementation Deps.mozilla_lib_crash implementation Deps.mozilla_lib_push_firebase diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt index 4812197db..4f5ee6e1e 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt @@ -11,7 +11,6 @@ import android.view.View import android.widget.PopupWindow import androidx.appcompat.content.res.AppCompatResources.getDrawable import kotlinx.android.synthetic.main.top_site_item.* -import kotlinx.android.synthetic.main.top_site_item.view.* import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.feature.top.sites.TopSite @@ -44,7 +43,7 @@ class TopSiteItemViewHolder( } top_site_item.setOnLongClickListener { - val menu = topSiteMenu.menuBuilder.build(view.context).show(anchor = it.top_site_title) + val menu = topSiteMenu.menuBuilder.build(view.context).show(anchor = it) it.setOnTouchListener @SuppressLint("ClickableViewAccessibility") { v, event -> onTouchEvent(v, event, menu) } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/view/LoginsListViewHolder.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/view/LoginsListViewHolder.kt index b5cd79943..223139995 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/view/LoginsListViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/view/LoginsListViewHolder.kt @@ -5,21 +5,18 @@ package org.mozilla.fenix.settings.logins.view import android.view.View -import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.logins_item.view.* +import kotlinx.android.synthetic.main.logins_item.* import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.loadIntoView import org.mozilla.fenix.settings.logins.SavedLogin import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor +import org.mozilla.fenix.utils.view.ViewHolder class LoginsListViewHolder( - private val view: View, + view: View, private val interactor: SavedLoginsInteractor -) : RecyclerView.ViewHolder(view) { +) : ViewHolder(view) { - private val favicon = view.favicon_image - private val url = view.webAddressView - private val username = view.usernameView private var loginItem: SavedLogin? = null fun bind(item: SavedLogin) { @@ -30,17 +27,17 @@ class LoginsListViewHolder( username = item.username, timeLastUsed = item.timeLastUsed ) - url.text = item.origin - username.text = item.username + webAddressView.text = item.origin + usernameView.text = item.username updateFavIcon(item.origin) - view.setOnClickListener { + itemView.setOnClickListener { interactor.onItemClicked(item) } } private fun updateFavIcon(url: String) { - favicon.context.components.core.icons.loadIntoView(favicon, url) + itemView.context.components.core.icons.loadIntoView(favicon_image, url) } } diff --git a/app/src/main/res/drawable/notification_indicator.xml b/app/src/main/res/drawable/notification_indicator.xml index 8090acf6d..e0641aac3 100644 --- a/app/src/main/res/drawable/notification_indicator.xml +++ b/app/src/main/res/drawable/notification_indicator.xml @@ -9,5 +9,5 @@ android:width="14dp" android:height="14dp" /> - + diff --git a/app/src/main/res/drawable/quick_action_icon_close.xml b/app/src/main/res/drawable/quick_action_icon_close.xml deleted file mode 100644 index 6ac688814..000000000 --- a/app/src/main/res/drawable/quick_action_icon_close.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/quick_action_icon_read_with_notification.xml b/app/src/main/res/drawable/quick_action_icon_read_with_notification.xml deleted file mode 100644 index 09560761b..000000000 --- a/app/src/main/res/drawable/quick_action_icon_read_with_notification.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/reader_two_state.xml b/app/src/main/res/drawable/reader_two_state.xml deleted file mode 100644 index 046ac7c9a..000000000 --- a/app/src/main/res/drawable/reader_two_state.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/reader_two_state_with_notification.xml b/app/src/main/res/drawable/reader_two_state_with_notification.xml deleted file mode 100644 index c25045f32..000000000 --- a/app/src/main/res/drawable/reader_two_state_with_notification.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/top_sites_background.xml b/app/src/main/res/drawable/top_sites_background.xml deleted file mode 100644 index 018b61714..000000000 --- a/app/src/main/res/drawable/top_sites_background.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/logins_item.xml b/app/src/main/res/layout/logins_item.xml index 08ed87f9f..cd64c9efe 100644 --- a/app/src/main/res/layout/logins_item.xml +++ b/app/src/main/res/layout/logins_item.xml @@ -12,27 +12,14 @@ android:focusable="true" android:minHeight="?android:attr/listPreferredItemHeight"> - - - + app:layout_constraintBottom_toBottomOf="parent" /> @@ -62,7 +49,7 @@ android:textColor="?secondaryText" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@id/favicon_wrapper" + app:layout_constraintStart_toEndOf="@+id/favicon_image" app:layout_constraintTop_toBottomOf="@id/webAddressView" app:layout_constraintVertical_chainStyle="packed" tools:text="mozilla.org" /> diff --git a/app/src/main/res/layout/top_site_item.xml b/app/src/main/res/layout/top_site_item.xml index 2267ce9d1..80303aa69 100644 --- a/app/src/main/res/layout/top_site_item.xml +++ b/app/src/main/res/layout/top_site_item.xml @@ -4,30 +4,17 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - - - + android:importantForAccessibility="no" /> @color/search_suggestion_indicator_icon_color_dark_theme @color/search_suggestion_indicator_icon_bookmark_color_dark_theme + @color/mozac_widget_favicon_background_dark_theme + @color/mozac_widget_favicon_border_dark_theme + @color/tab_tray_item_text_dark_theme @color/tab_tray_item_url_dark_theme @@ -65,8 +68,6 @@ @color/tab_tray_selected_mask_dark_theme - @color/top_site_background_dark_theme - @color/top_site_border_dark_theme @color/top_site_title_text_dark_theme diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9a22639e5..4e7101f93 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -4,21 +4,8 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - #5B5B66 - #52525E - #32313C - #1C1B22 #312A65 #7A312A65 - #291D4F - #20123A - #1D1133 - #FBFBFE - #F9F9FB - #E0E0E6 - #AFAFBB - #8F8F9D - #80808E #9059FF #529059FF #7A9059FF @@ -45,10 +32,8 @@ #F542414D #FF15141A #0015141A - @android:color/white - @color/light_grey_30 - @color/light_grey_30 - @color/light_grey_80 + @color/photonLightGrey30 + @color/photonLightGrey80 #7542E5 #0250BB #E31587 @@ -74,19 +59,22 @@ #008787 #0060df + @color/photonWhite + @color/photonLightGrey30 + - @color/ink_80 - @color/dark_grey_05 - @color/light_grey_10 + @color/photonInk80 + @color/photonDarkGrey05 + @color/photonLightGrey10 #E5DFF4 - @color/light_grey_10 - @color/light_grey_30 + @color/photonLightGrey10 + @color/photonLightGrey30 #ffffff #312A65 @color/ink_20 @color/ink_20_48a - @color/light_grey_10 - @color/light_grey_60 + @color/photonLightGrey10 + @color/photonLightGrey60 @color/violet_70_12a @@ -108,10 +96,8 @@ #66FBFBFE #F520123A #F515141A - @color/dark_grey_50 - @color/dark_grey_10 - @color/dark_grey_10 - @color/light_grey_90 + @color/photonDarkGrey10 + @color/photonLightGrey90 #AB71FF #00B3F4 #FF6BBA @@ -137,18 +123,21 @@ #2ac3a2 #0090ed + @color/photonDarkGrey50 + @color/photonDarkGrey10 + - @color/light_grey_05 - @color/light_grey_60 - @color/dark_grey_80 + @color/photonLightGrey05 + @color/photonLightGrey60 + @color/photonDarkGrey80 #412E69 - @color/dark_grey_50 - @color/dark_grey_10 + @color/photonDarkGrey50 + @color/photonDarkGrey10 #9059FF @color/violet_50 @color/violet_50_48a - @color/dark_grey_50 - @color/dark_grey_05 + @color/photonDarkGrey50 + @color/photonDarkGrey05 @color/violet_50_32a @@ -156,8 +145,8 @@ #A7A2B7 @color/primary_text_private_theme #261E4B - #291D4F - #291D4F + @color/photonInk50 + @color/photonInk50 #9059FF #592ACB #AB71FF @@ -192,13 +181,16 @@ #FFFFFF #9059ff + @color/photonInk50 + @color/photonInk50 + - @color/light_grey_05 - @color/light_grey_60 - @color/ink_90 + @color/photonLightGrey05 + @color/photonLightGrey60 + @color/photonInk90 #422875 - @color/ink_50 - @color/dark_grey_10 + @color/photonInk50 + @color/photonDarkGrey10 #9059FF @color/violet_50 @color/violet_50_48a @@ -245,6 +237,9 @@ @color/search_suggestion_indicator_icon_color_light_theme @color/search_suggestion_indicator_icon_bookmark_color_light_theme + @color/mozac_widget_favicon_background_light_theme + @color/mozac_widget_favicon_border_light_theme + @color/tab_tray_item_text_light_theme @color/tab_tray_item_url_light_theme @@ -263,8 +258,6 @@ #DFDFE3 - @color/top_site_background_light_theme - @color/top_site_border_light_theme @color/top_site_title_text_light_theme @@ -330,8 +323,8 @@ @color/photonGrey10 @color/dark_grey_90 - @color/light_grey_05 - @color/light_grey_05 + @color/photonLightGrey05 + @color/photonLightGrey05 @color/toggle_off_knob_light_theme @color/toggle_off_track_light_theme diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a11de8c72..b67e9349e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -80,6 +80,10 @@ @color/search_suggestion_indicator_icon_color_normal_theme @color/search_suggestion_indicator_icon_bookmark_color_normal_theme + + @color/mozac_widget_favicon_background_normal_theme + @color/mozac_widget_favicon_border_normal_theme + @color/tab_tray_item_background_normal_theme @color/tab_tray_item_selected_background_normal_theme @color/tab_tray_toolbar_background_normal_theme @@ -213,6 +217,10 @@ @color/search_suggestion_indicator_icon_color_dark_theme @color/search_suggestion_indicator_icon_bookmark_color_dark_theme + + @color/mozac_widget_favicon_background_private_theme + @color/mozac_widget_favicon_border_private_theme + @color/tab_tray_item_background_normal_theme @color/tab_tray_item_selected_background_private_theme diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 043375927..b6540a220 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -132,6 +132,7 @@ object Deps { const val mozilla_ui_colors = "org.mozilla.components:ui-colors:${Versions.mozilla_android_components}" const val mozilla_ui_icons = "org.mozilla.components:ui-icons:${Versions.mozilla_android_components}" + const val mozilla_ui_widgets = "org.mozilla.components:ui-widgets:${Versions.mozilla_android_components}" const val mozilla_lib_crash = "org.mozilla.components:lib-crash:${Versions.mozilla_android_components}" const val mozilla_lib_push_firebase = "org.mozilla.components:lib-push-firebase:${Versions.mozilla_android_components}" From 3d3153039ce794df09a243968b888ae7cb856d9b Mon Sep 17 00:00:00 2001 From: ekager Date: Thu, 30 Jul 2020 14:02:38 -0400 Subject: [PATCH 36/74] For #11690 - Add contentsSameAs to avoid rebind of items in SessionControlAdapter --- .../sessioncontrol/SessionControlAdapter.kt | 90 +++++++++++++------ 1 file changed, 64 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index c9441f99e..352b3ad19 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt @@ -15,7 +15,6 @@ import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.home.OnboardingState -import org.mozilla.fenix.home.Tab import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder @@ -38,8 +37,17 @@ import mozilla.components.feature.tab.collections.Tab as ComponentTab sealed class AdapterItem(@LayoutRes val viewType: Int) { data class TipItem(val tip: Tip) : AdapterItem( - ButtonTipViewHolder.LAYOUT_ID) - data class TopSiteList(val topSites: List) : AdapterItem(TopSiteViewHolder.LAYOUT_ID) + ButtonTipViewHolder.LAYOUT_ID + ) + + data class TopSiteList(val topSites: List) : AdapterItem(TopSiteViewHolder.LAYOUT_ID) { + override fun contentsSameAs(other: AdapterItem): Boolean { + val newTopSites = (other as? TopSiteList)?.topSites?.asSequence() ?: return false + val oldTopSites = this.topSites.asSequence() + return newTopSites.zip(oldTopSites).all { (new, old) -> new.title == old.title } + } + } + object PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID) object NoCollectionsMessage : AdapterItem(NoCollectionsMessageViewHolder.LAYOUT_ID) @@ -48,32 +56,48 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) { val collection: TabCollection, val expanded: Boolean ) : AdapterItem(CollectionViewHolder.LAYOUT_ID) { - override fun sameAs(other: AdapterItem) = other is CollectionItem && collection.id == other.collection.id + override fun sameAs(other: AdapterItem) = + other is CollectionItem && collection.id == other.collection.id + + override fun contentsSameAs(other: AdapterItem): Boolean { + (other as? CollectionItem)?.let { + return it.expanded == this.expanded && it.collection.title == this.collection.title + } ?: return false + } } + data class TabInCollectionItem( val collection: TabCollection, val tab: ComponentTab, val isLastTab: Boolean ) : AdapterItem(TabInCollectionViewHolder.LAYOUT_ID) { - override fun sameAs(other: AdapterItem) = other is TabInCollectionItem && tab.id == other.tab.id + override fun sameAs(other: AdapterItem) = + other is TabInCollectionItem && tab.id == other.tab.id } object OnboardingHeader : AdapterItem(OnboardingHeaderViewHolder.LAYOUT_ID) data class OnboardingSectionHeader( val labelBuilder: (Context) -> String ) : AdapterItem(OnboardingSectionHeaderViewHolder.LAYOUT_ID) { - override fun sameAs(other: AdapterItem) = other is OnboardingSectionHeader && labelBuilder == other.labelBuilder + override fun sameAs(other: AdapterItem) = + other is OnboardingSectionHeader && labelBuilder == other.labelBuilder } + object OnboardingManualSignIn : AdapterItem(OnboardingManualSignInViewHolder.LAYOUT_ID) data class OnboardingAutomaticSignIn( val state: OnboardingState.SignedOutCanAutoSignIn ) : AdapterItem(OnboardingAutomaticSignInViewHolder.LAYOUT_ID) + object OnboardingThemePicker : AdapterItem(OnboardingThemePickerViewHolder.LAYOUT_ID) - object OnboardingTrackingProtection : AdapterItem(OnboardingTrackingProtectionViewHolder.LAYOUT_ID) + object OnboardingTrackingProtection : + AdapterItem(OnboardingTrackingProtectionViewHolder.LAYOUT_ID) + object OnboardingPrivateBrowsing : AdapterItem(OnboardingPrivateBrowsingViewHolder.LAYOUT_ID) object OnboardingPrivacyNotice : AdapterItem(OnboardingPrivacyNoticeViewHolder.LAYOUT_ID) object OnboardingFinish : AdapterItem(OnboardingFinishViewHolder.LAYOUT_ID) - object OnboardingToolbarPositionPicker : AdapterItem(OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID) + object OnboardingToolbarPositionPicker : + AdapterItem(OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID) + object OnboardingWhatsNew : AdapterItem(OnboardingWhatsNewViewHolder.LAYOUT_ID) /** @@ -85,26 +109,21 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) { * Returns a payload if there's been a change, or null if not */ open fun getChangePayload(newItem: AdapterItem): Any? = null + + open fun contentsSameAs(other: AdapterItem) = this::class == other::class } class AdapterItemDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = oldItem.sameAs(newItem) + override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = + oldItem.sameAs(newItem) @Suppress("DiffUtilEquals") - override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = oldItem == newItem + override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = + oldItem.contentsSameAs(newItem) override fun getChangePayload(oldItem: AdapterItem, newItem: AdapterItem): Any? { return oldItem.getChangePayload(newItem) ?: return super.getChangePayload(oldItem, newItem) } - - data class TabChangePayload( - val tab: Tab, - val shouldUpdateFavicon: Boolean, - val shouldUpdateHostname: Boolean, - val shouldUpdateTitle: Boolean, - val shouldUpdateSelected: Boolean, - val shouldUpdateMediaState: Boolean - ) } class SessionControlAdapter( @@ -119,23 +138,42 @@ class SessionControlAdapter( return when (viewType) { ButtonTipViewHolder.LAYOUT_ID -> ButtonTipViewHolder(view, interactor) TopSiteViewHolder.LAYOUT_ID -> TopSiteViewHolder(view, interactor) - PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(view, interactor) + PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder( + view, + interactor + ) NoCollectionsMessageViewHolder.LAYOUT_ID -> NoCollectionsMessageViewHolder(view, interactor, hasNormalTabsOpened) CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view) CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, interactor) - TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder(view, interactor, differentLastItem = true) + TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder( + view, + interactor, + differentLastItem = true + ) OnboardingHeaderViewHolder.LAYOUT_ID -> OnboardingHeaderViewHolder(view) OnboardingSectionHeaderViewHolder.LAYOUT_ID -> OnboardingSectionHeaderViewHolder(view) - OnboardingAutomaticSignInViewHolder.LAYOUT_ID -> OnboardingAutomaticSignInViewHolder(view) + OnboardingAutomaticSignInViewHolder.LAYOUT_ID -> OnboardingAutomaticSignInViewHolder( + view + ) OnboardingManualSignInViewHolder.LAYOUT_ID -> OnboardingManualSignInViewHolder(view) OnboardingThemePickerViewHolder.LAYOUT_ID -> OnboardingThemePickerViewHolder(view) - OnboardingTrackingProtectionViewHolder.LAYOUT_ID -> OnboardingTrackingProtectionViewHolder(view) - OnboardingPrivateBrowsingViewHolder.LAYOUT_ID -> OnboardingPrivateBrowsingViewHolder(view, interactor) - OnboardingPrivacyNoticeViewHolder.LAYOUT_ID -> OnboardingPrivacyNoticeViewHolder(view, interactor) + OnboardingTrackingProtectionViewHolder.LAYOUT_ID -> OnboardingTrackingProtectionViewHolder( + view + ) + OnboardingPrivateBrowsingViewHolder.LAYOUT_ID -> OnboardingPrivateBrowsingViewHolder( + view, + interactor + ) + OnboardingPrivacyNoticeViewHolder.LAYOUT_ID -> OnboardingPrivacyNoticeViewHolder( + view, + interactor + ) OnboardingFinishViewHolder.LAYOUT_ID -> OnboardingFinishViewHolder(view, interactor) OnboardingWhatsNewViewHolder.LAYOUT_ID -> OnboardingWhatsNewViewHolder(view, interactor) - OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder(view) + OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder( + view + ) else -> throw IllegalStateException() } } From 6cdf304d10826c9df521b9f59fa8415cf79d8170 Mon Sep 17 00:00:00 2001 From: ekager Date: Thu, 30 Jul 2020 17:08:33 -0400 Subject: [PATCH 37/74] No issue: Adds sameAs function for TopSiteList AdapterItem --- .../home/sessioncontrol/SessionControlAdapter.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index 352b3ad19..6acca7d2e 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt @@ -41,10 +41,17 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) { ) data class TopSiteList(val topSites: List) : AdapterItem(TopSiteViewHolder.LAYOUT_ID) { + override fun sameAs(other: AdapterItem): Boolean { + val newTopSites = (other as? TopSiteList) ?: return false + return newTopSites.topSites == this.topSites + } + override fun contentsSameAs(other: AdapterItem): Boolean { - val newTopSites = (other as? TopSiteList)?.topSites?.asSequence() ?: return false + val newTopSites = (other as? TopSiteList) ?: return false + if (newTopSites.topSites.size != this.topSites.size) return false + val newSitesSequence = newTopSites.topSites.asSequence() val oldTopSites = this.topSites.asSequence() - return newTopSites.zip(oldTopSites).all { (new, old) -> new.title == old.title } + return newSitesSequence.zip(oldTopSites).all { (new, old) -> new.title == old.title } } } From 4c1e6eeb25eeccdb60f78c1a72fdcb26ee349a2c Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Thu, 30 Jul 2020 09:59:37 -0700 Subject: [PATCH 38/74] For #12887: Use ListAdapter to diff history --- .../fenix/library/LibrarySiteItemView.kt | 5 ++++ .../fenix/tabhistory/TabHistoryAdapter.kt | 25 +++++++++-------- .../fenix/tabhistory/TabHistoryView.kt | 12 ++++---- .../fenix/tabhistory/TabHistoryViewHolder.kt | 28 +++++++++++-------- 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/library/LibrarySiteItemView.kt b/app/src/main/java/org/mozilla/fenix/library/LibrarySiteItemView.kt index 239b9036e..6c6b1915d 100644 --- a/app/src/main/java/org/mozilla/fenix/library/LibrarySiteItemView.kt +++ b/app/src/main/java/org/mozilla/fenix/library/LibrarySiteItemView.kt @@ -67,6 +67,8 @@ class LibrarySiteItemView @JvmOverloads constructor( val overflowView: ImageButton get() = overflow_menu + private var iconUrl: String? = null + init { LayoutInflater.from(context).inflate(R.layout.library_site_item, this, true) @@ -94,6 +96,9 @@ class LibrarySiteItemView @JvmOverloads constructor( } fun loadFavicon(url: String) { + if (iconUrl == url) return + + iconUrl = url context.components.core.icons.loadIntoView(favicon, url) } diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt index 433c620e1..d47519ea7 100644 --- a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt @@ -6,7 +6,8 @@ package org.mozilla.fenix.tabhistory import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter import org.mozilla.fenix.R data class TabHistoryItem( @@ -18,23 +19,23 @@ data class TabHistoryItem( class TabHistoryAdapter( private val interactor: TabHistoryViewInteractor -) : RecyclerView.Adapter() { - - var historyList: List = emptyList() - set(value) { - field = value - notifyDataSetChanged() - } +) : ListAdapter(DiffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabHistoryViewHolder { - val view = - LayoutInflater.from(parent.context).inflate(R.layout.history_list_item, parent, false) + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.history_list_item, parent, false) return TabHistoryViewHolder(view, interactor) } override fun onBindViewHolder(holder: TabHistoryViewHolder, position: Int) { - holder.bind(historyList[position]) + holder.bind(getItem(position)) } - override fun getItemCount(): Int = historyList.size + internal object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: TabHistoryItem, newItem: TabHistoryItem) = + oldItem.url == newItem.url + + override fun areContentsTheSame(oldItem: TabHistoryItem, newItem: TabHistoryItem) = + oldItem == newItem + } } diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryView.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryView.kt index c02ba038d..cb87a40a5 100644 --- a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryView.kt @@ -24,19 +24,16 @@ interface TabHistoryViewInteractor { } class TabHistoryView( - private val container: ViewGroup, + container: ViewGroup, private val expandDialog: () -> Unit, interactor: TabHistoryViewInteractor ) : LayoutContainer { - override val containerView: View? - get() = container - - val view: View = LayoutInflater.from(container.context) + override val containerView: View = LayoutInflater.from(container.context) .inflate(R.layout.component_tabhistory, container, true) private val adapter = TabHistoryAdapter(interactor) - private val layoutManager = object : LinearLayoutManager(view.context) { + private val layoutManager = object : LinearLayoutManager(containerView.context) { override fun onLayoutCompleted(state: RecyclerView.State?) { super.onLayoutCompleted(state) currentIndex?.let { index -> @@ -60,6 +57,7 @@ class TabHistoryView( init { tabHistoryRecyclerView.adapter = adapter tabHistoryRecyclerView.layoutManager = layoutManager + tabHistoryRecyclerView.itemAnimator = null } fun updateState(state: BrowserState) { @@ -73,7 +71,7 @@ class TabHistoryView( isSelected = index == historyState.currentIndex ) } - adapter.historyList = items + adapter.submitList(items) } } } diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt index 88bd26d55..1db9bdada 100644 --- a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt @@ -7,28 +7,34 @@ package org.mozilla.fenix.tabhistory import android.view.View import androidx.core.text.bold import androidx.core.text.buildSpannedString -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.history_list_item.view.* +import kotlinx.android.synthetic.main.history_list_item.* +import org.mozilla.fenix.library.LibrarySiteItemView +import org.mozilla.fenix.utils.view.ViewHolder class TabHistoryViewHolder( - private val view: View, + view: View, private val interactor: TabHistoryViewInteractor -) : RecyclerView.ViewHolder(view) { +) : ViewHolder(view) { + + private lateinit var item: TabHistoryItem + + init { + itemView.setOnClickListener { interactor.goToHistoryItem(item) } + } fun bind(item: TabHistoryItem) { - view.history_layout.overflowView.isVisible = false - view.history_layout.urlView.text = item.url - view.history_layout.loadFavicon(item.url) + this.item = item - view.history_layout.titleView.text = if (item.isSelected) { + history_layout.displayAs(LibrarySiteItemView.ItemType.SITE) + history_layout.urlView.text = item.url + history_layout.loadFavicon(item.url) + + history_layout.titleView.text = if (item.isSelected) { buildSpannedString { bold { append(item.title) } } } else { item.title } - - view.setOnClickListener { interactor.goToHistoryItem(item) } } } From 6152469ffd309481bd5760d999972ad7104c1201 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Thu, 30 Jul 2020 10:59:25 -0700 Subject: [PATCH 39/74] Add tests for tab history --- .../fenix/tabhistory/TabHistoryAdapterTest.kt | 96 +++++++++++++++++++ .../tabhistory/TabHistoryControllerTest.kt | 14 +-- .../tabhistory/TabHistoryViewHolderTest.kt | 62 ++++++++++++ 3 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryAdapterTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolderTest.kt diff --git a/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryAdapterTest.kt new file mode 100644 index 000000000..72662e807 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryAdapterTest.kt @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.tabhistory + +import android.text.Spanned +import android.widget.FrameLayout +import androidx.appcompat.view.ContextThemeWrapper +import androidx.core.text.HtmlCompat +import io.mockk.mockk +import kotlinx.android.synthetic.main.history_list_item.* +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class TabHistoryAdapterTest { + + private lateinit var parent: FrameLayout + private lateinit var interactor: TabHistoryInteractor + private lateinit var adapter: TabHistoryAdapter + + private val selectedItem = TabHistoryItem( + title = "Mozilla", + url = "https://mozilla.org", + index = 0, + isSelected = true + ) + private val unselectedItem = TabHistoryItem( + title = "Firefox", + url = "https://firefox.com", + index = 1, + isSelected = false + ) + + @Before + fun setup() { + parent = FrameLayout(ContextThemeWrapper(testContext, R.style.NormalTheme)) + interactor = mockk() + adapter = TabHistoryAdapter(interactor) + } + + @Test + fun `creates and binds view holder`() { + adapter.submitList(listOf(selectedItem, unselectedItem)) + + val holder = adapter.createViewHolder(parent, 0) + + adapter.bindViewHolder(holder, 0) + val htmlSelected = HtmlCompat.toHtml(holder.history_layout.titleView.text as Spanned, 0) + assertTrue(htmlSelected, "Mozilla" in htmlSelected) + + adapter.bindViewHolder(holder, 1) + assertFalse(holder.history_layout.titleView.text is Spanned) + assertEquals("Firefox", holder.history_layout.titleView.text) + } + + @Test + fun `items are the same if they have matching URLs`() { + assertTrue(TabHistoryAdapter.DiffCallback.areItemsTheSame( + selectedItem, + selectedItem + )) + assertTrue(TabHistoryAdapter.DiffCallback.areItemsTheSame( + unselectedItem, + unselectedItem.copy(title = "Waterbug", index = 2, isSelected = true) + )) + assertFalse(TabHistoryAdapter.DiffCallback.areItemsTheSame( + unselectedItem, + unselectedItem.copy(url = "https://firefox.com/subpage") + )) + } + + @Test + fun `equal items have the same contents`() { + assertTrue(TabHistoryAdapter.DiffCallback.areContentsTheSame( + selectedItem, + selectedItem + )) + assertFalse(TabHistoryAdapter.DiffCallback.areContentsTheSame( + selectedItem, + selectedItem.copy(title = "Waterbug", index = 2, isSelected = false) + )) + assertFalse(TabHistoryAdapter.DiffCallback.areContentsTheSame( + unselectedItem, + selectedItem + )) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryControllerTest.kt index 8b32cc10f..1f9099245 100644 --- a/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryControllerTest.kt @@ -10,19 +10,20 @@ import io.mockk.verify import mozilla.components.browser.session.SessionManager import mozilla.components.feature.session.SessionUseCases import org.junit.Test +import org.mozilla.fenix.R class TabHistoryControllerTest { - val sessionManager: SessionManager = mockk(relaxed = true) - val navController: NavController = mockk(relaxed = true) - val sessionUseCases = SessionUseCases(sessionManager) - val goToHistoryIndexUseCase = sessionUseCases.goToHistoryIndex - val controller = DefaultTabHistoryController( + private val sessionManager: SessionManager = mockk(relaxed = true) + private val navController: NavController = mockk(relaxed = true) + private val sessionUseCases = SessionUseCases(sessionManager) + private val goToHistoryIndexUseCase = sessionUseCases.goToHistoryIndex + private val controller = DefaultTabHistoryController( navController = navController, goToHistoryIndexUseCase = goToHistoryIndexUseCase ) - val currentItem = TabHistoryItem( + private val currentItem = TabHistoryItem( index = 0, title = "", url = "", @@ -33,6 +34,7 @@ class TabHistoryControllerTest { fun handleGoToHistoryIndex() { controller.handleGoToHistoryItem(currentItem) + verify { navController.popBackStack(R.id.browserFragment, false) } verify { goToHistoryIndexUseCase.invoke(currentItem.index) } } } diff --git a/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolderTest.kt new file mode 100644 index 000000000..e3013d60e --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolderTest.kt @@ -0,0 +1,62 @@ +package org.mozilla.fenix.tabhistory + +import android.view.View +import io.mockk.CapturingSlot +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import org.junit.Before +import org.junit.Test +import org.mozilla.fenix.R +import org.mozilla.fenix.library.LibrarySiteItemView + +class TabHistoryViewHolderTest { + + @MockK private lateinit var view: View + @MockK private lateinit var interactor: TabHistoryViewInteractor + @MockK(relaxed = true) private lateinit var siteItemView: LibrarySiteItemView + private lateinit var holder: TabHistoryViewHolder + private lateinit var onClick: CapturingSlot + + @Before + fun setup() { + MockKAnnotations.init(this) + onClick = slot() + + every { view.setOnClickListener(capture(onClick)) } just Runs + every { view.findViewById(R.id.history_layout) } returns siteItemView + + holder = TabHistoryViewHolder(view, interactor) + } + + @Test + fun `calls interactor on click`() { + every { interactor.goToHistoryItem(any()) } just Runs + + val item = mockk(relaxed = true) + holder.bind(item) + onClick.captured.onClick(mockk()) + verify { interactor.goToHistoryItem(item) } + } + + @Test + fun `binds title and url`() { + val item = TabHistoryItem( + title = "Firefox", + url = "https://firefox.com", + index = 1, + isSelected = false + ) + holder.bind(item) + + verify { siteItemView.displayAs(LibrarySiteItemView.ItemType.SITE) } + verify { siteItemView.titleView.text = "Firefox" } + verify { siteItemView.urlView.text = "https://firefox.com" } + verify { siteItemView.loadFavicon("https://firefox.com") } + } +} From 5ddf8beea7432826e20ca4e46e8bba29338f0ef2 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Thu, 30 Jul 2020 14:44:41 -0700 Subject: [PATCH 40/74] Fix suggestions, extract layout for tab history --- .../org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt | 2 +- .../org/mozilla/fenix/tabhistory/TabHistoryView.kt | 1 - .../mozilla/fenix/tabhistory/TabHistoryViewHolder.kt | 6 ++++-- app/src/main/res/layout/tab_history_list_item.xml | 11 +++++++++++ .../fenix/tabhistory/TabHistoryViewHolderTest.kt | 2 +- 5 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 app/src/main/res/layout/tab_history_list_item.xml diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt index d47519ea7..96e130079 100644 --- a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt @@ -23,7 +23,7 @@ class TabHistoryAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabHistoryViewHolder { val view = LayoutInflater.from(parent.context) - .inflate(R.layout.history_list_item, parent, false) + .inflate(R.layout.tab_history_list_item, parent, false) return TabHistoryViewHolder(view, interactor) } diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryView.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryView.kt index cb87a40a5..c7e2e3004 100644 --- a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryView.kt @@ -57,7 +57,6 @@ class TabHistoryView( init { tabHistoryRecyclerView.adapter = adapter tabHistoryRecyclerView.layoutManager = layoutManager - tabHistoryRecyclerView.itemAnimator = null } fun updateState(state: BrowserState) { diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt index 1db9bdada..cfa2cc81a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt @@ -7,7 +7,8 @@ package org.mozilla.fenix.tabhistory import android.view.View import androidx.core.text.bold import androidx.core.text.buildSpannedString -import kotlinx.android.synthetic.main.history_list_item.* +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.tab_history_list_item.* import org.mozilla.fenix.library.LibrarySiteItemView import org.mozilla.fenix.utils.view.ViewHolder @@ -19,13 +20,14 @@ class TabHistoryViewHolder( private lateinit var item: TabHistoryItem init { - itemView.setOnClickListener { interactor.goToHistoryItem(item) } + history_layout.setOnClickListener { interactor.goToHistoryItem(item) } } fun bind(item: TabHistoryItem) { this.item = item history_layout.displayAs(LibrarySiteItemView.ItemType.SITE) + history_layout.overflowView.isVisible = false history_layout.urlView.text = item.url history_layout.loadFavicon(item.url) diff --git a/app/src/main/res/layout/tab_history_list_item.xml b/app/src/main/res/layout/tab_history_list_item.xml new file mode 100644 index 000000000..3bdef8369 --- /dev/null +++ b/app/src/main/res/layout/tab_history_list_item.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolderTest.kt index e3013d60e..e6598c1b9 100644 --- a/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolderTest.kt @@ -28,7 +28,7 @@ class TabHistoryViewHolderTest { MockKAnnotations.init(this) onClick = slot() - every { view.setOnClickListener(capture(onClick)) } just Runs + every { siteItemView.setOnClickListener(capture(onClick)) } just Runs every { view.findViewById(R.id.history_layout) } returns siteItemView holder = TabHistoryViewHolder(view, interactor) From 29d07e8153da16be6fc8d0e6e7fdcdcb3c74f59b Mon Sep 17 00:00:00 2001 From: Mozilla L10n Automation Bot Date: Fri, 31 Jul 2020 00:01:06 +0000 Subject: [PATCH 41/74] Import l10n. --- app/src/main/res/values-cy/strings.xml | 27 +++++++++++++++++++ app/src/main/res/values-iw/strings.xml | 17 ++++++++++++ app/src/main/res/values-kn/strings.xml | 30 +++++++++++++++------- app/src/main/res/values-oc/strings.xml | 17 ++++++++++++ app/src/main/res/values-zh-rCN/strings.xml | 4 +-- app/src/main/res/values-zh-rTW/strings.xml | 2 +- 6 files changed, 85 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 78f8fb1a3..7373f9449 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -24,6 +24,30 @@ %1$s tab agored. Tapio i newid tabiau. + + %1$d wedi’u dewis + + Ychwanegu casgliad newydd + + Enw + + Dewis casgliad + + Gadael y modd aml-ddewis + + Cadw’r tabiau hyn i gasgliad + + + Wedi dewis %1$s + + Dad-ddewis %1$s + + Wedi gadael y modd aml-ddewis + + Modd aml-ddewis wedi’i ddewis, dewiswch dabiau i’w cadw i gasgliad + + Dewiswyd + Mae %1$s yn cael ei greu gan Mozilla. @@ -508,6 +532,9 @@ %1$s (Modd Preifat) + + Cadw + Dileu hanes diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index a5acf32f2..7074492e3 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -24,6 +24,8 @@ %1$s לשוניות פתוחות. יש להקיש כדי להחליף לשוניות. + + %1$d נבחרו הוספת אוסף חדש @@ -35,11 +37,18 @@ שמירת הלשוניות שנבחרו לאוסף + + נבחר %1$s + + בוטלה הבחירה של %1$s בוצעה יציאה ממצב בחירה מרובה בוצעה כניסה למצב בחירה מרובה, יש לבחור בלשוניות כדי לשמור לאוסף + + נבחר + ‏%1$s נוצר על־ידי Mozilla. @@ -64,6 +73,8 @@ לא תודה + + הוספת וידג׳ט לא כעת @@ -710,6 +721,10 @@ אוספים תפריט אוסף + + לאסוף את הדברים החשובים לך + + ניתן לקבץ חיפושים, אתרים ולשוניות דומים יחד כדי לגשת אליהם מהר יותר בהמשך. בחירת לשוניות @@ -1280,6 +1295,8 @@ הסתרת ססמה יש לבטל את הנעילה כדי להציג את הכניסות השמורות שלך + + שמירה מאובטחת של הכניסות והססמאות שלך מאוחר יותר diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index b0c9cd031..f32501763 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -142,13 +142,9 @@ ಸ್ಕ್ಯಾನ್ - ಕಿರುಹಾದಿಗಳು + ಸರ್ಚ್ ಎಂಜಿನ್ ಸರ್ಚ್ ಎಂಜಿನ್ ಸಿದ್ದತೆಗಳು - - ಇದರೊಂದಿಗೆ ಹುಡುಕು - - ಈ ಸಮಯದಲ್ಲಿ, ಇದರೊಂದಿಗೆ ಹುಡುಕಿ: ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ನಿಂದ ಲಿಂಕ್ ಅನ್ನು ಭರ್ತಿ ಮಾಡಿ @@ -254,8 +250,6 @@ ವಿಕಸನಾ ಉಪಕರಣಗಳು ರಿಮೋಟ್ ಡೀಬಗ್ಗಿಂಗ್ USB ಮೂಲಕ - - ಹುಡುಕಾಟ ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ತೋರಿಸಿ ಹುಡುಕಲು ಸಲಹೆಗಳನ್ನು ತೋರಿಸು @@ -498,6 +492,9 @@ %1$s (ಖಾಸಗಿ ಮೋಡ್) + + ಉಳಿಸು + ಇತಿಹಾಸವನ್ನು ಅಳಿಸಿ @@ -618,7 +615,7 @@ %1$s ಅಳಿಸಲಾಗಿದೆ - + ಬುಕ್‌ಮಾರ್ಕ್‌ಗಳನ್ನು ಅಳಿಸಲಾಗಿದೆ ರದ್ದುಗೊಳಿಸಿ @@ -712,6 +709,8 @@ %d ಟ್ಯಾಬ್‌ಗಳನ್ನು ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆ ಟ್ಯಾಬ್‌ಗಳನ್ನು ಉಳಿಸಲಾಗಿದೆ! + + ಸಂಗ್ರಹವನ್ನು ಉಳಿಸಲಾಗಿದೆ! ಟ್ಯಾಬ್‌ಗಳನ್ನು ಉಳಿಸಲಾಗಿದೆ! @@ -816,6 +815,8 @@ ನಿರಾಕರಿಸು %1$s ಅನ್ನು ಅಳಿಸಲು ನೀವು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ? + + ‍%1$s ಅನ್ನು ಅಳಿಸುವುದೇ? ಅಳಿಸು @@ -1208,6 +1209,8 @@ ಉಳಿಸದ ಲಾಗಿನ್‌ಗಳು ಮತ್ತು ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಇಲ್ಲಿ ತೋರಿಸಲಾಗುತ್ತದೆ. ಈ ಸೈಟ್‌ಗಳಿಗೆ ಲಾಗಿನ್‌ಗಳು ಮತ್ತು ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಉಳಿಸಲಾಗುವುದಿಲ್ಲ. + + ಎಲ್ಲಾ ವಿನಾಯಿತಿಗಳನ್ನು ಅಳಿಸಿ ಹುಡುಕು ಎಂಜಿನ್‌ಗಳು @@ -1418,4 +1421,13 @@ ಸರಿ, ಗೊತ್ತಾಯಿತು - + + + ಕಿರುಹಾದಿಗಳು + + ಇದರೊಂದಿಗೆ ಹುಡುಕು + + ಈ ಸಮಯದಲ್ಲಿ, ಇದರೊಂದಿಗೆ ಹುಡುಕಿ: + + ಹುಡುಕಾಟ ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ತೋರಿಸಿ + diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index 4bca5eab7..a03448f1f 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -35,6 +35,10 @@ Seleccionar una colleccion Enregistrar los onglets seleccionats dins la colleccion + + %1$s seleccionat + + %1$s deseleccionat Seleccionat @@ -929,6 +933,9 @@ Firefox Nightly a mudat los catons + + Passar la Nightly nòu + Firefox Nightly a mudat los catons @@ -1019,6 +1026,10 @@ Connectatz-vos amb la camèra Utilizar una adreça electronica allòc + + Firefox quitarà de sincronizar vòstre compte mas escafarà pas las donadas de navegacion d’aqueste periferic. + + %s quitarà de sincronizar vòstre compte mas escafarà pas las donadas de navegacion d’aqueste periferic. Se desconectar @@ -1034,14 +1045,20 @@ Proteccion renfortida contra lo seguiment Navegatz sens èsser seguit + + Gardatz vòstras donadas per vos. %s vos protegís de la màger part dels traçadors mai comuns que vos seguisson en linha. Ne saber mai Estandarda (per defaut) + + Bloca mens de traçadors. Las paginas se cargan normalament. Çò que la proteccion contra lo seguiment estandarda bloca Estricte + + Bloca mai d’elements de seguiment, publicitats e fenèstras sorgissentas. Las paginas se cargament rapidament, mas pòt arribar que d’unas foncionalitats sián copadas. Çò que la proteccion contra lo seguiment estricta bloca diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8bc09e8ae..ca47a107c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -171,7 +171,7 @@ 搜索 - 依照设备语言显示 + 依照设备语言显示 (ISO 3166 / 639) 搜索语言 @@ -282,7 +282,7 @@ 重新连接以恢复同步 - 语言(地区) + 语言 数据反馈 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index b13bb5b92..e1d52cb6b 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -29,7 +29,7 @@ 開啟了 %1$s 個分頁,點擊即可切換分頁。 - 已選擇 %1$d 個分頁 + 已選擇 %1$d 個分頁 新增收藏集 From bffb56633a4c9d11e00fa13eb6f4674e3d2a2ba4 Mon Sep 17 00:00:00 2001 From: MickeyMoz Date: Fri, 31 Jul 2020 14:11:29 +0000 Subject: [PATCH 42/74] Update Android Components version to 53.0.20200731130051. --- buildSrc/src/main/java/AndroidComponents.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/AndroidComponents.kt b/buildSrc/src/main/java/AndroidComponents.kt index ca756537a..0af20d7cb 100644 --- a/buildSrc/src/main/java/AndroidComponents.kt +++ b/buildSrc/src/main/java/AndroidComponents.kt @@ -3,5 +3,5 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ object AndroidComponents { - const val VERSION = "53.0.20200729190533" + const val VERSION = "53.0.20200731130051" } From 58e24b81aa1df3a628856e3fdd29aebccd089f61 Mon Sep 17 00:00:00 2001 From: Mihai Eduard Badea Date: Tue, 14 Jul 2020 11:42:02 +0300 Subject: [PATCH 43/74] For issue #12400 - Refresh swiped collection tab view Item is now refreshed by calling notifyDataSetChanged on the adapter when the last tab from the collection has been swiped away and the user cancels the deletion by pressing the cancel button from the dialog. Also added a "wasSwiped" flag to onCollectionRemoveTab in order to check if the tab was deleted from a swipe action and not by pressing the "X" button. --- .../org/mozilla/fenix/home/HomeFragment.kt | 18 +++++++++++-- .../SessionControlController.kt | 19 ++++++++----- .../SessionControlInteractor.kt | 6 ++--- .../sessioncontrol/SwipeToDeleteCallback.kt | 2 +- .../viewholders/TabInCollectionViewHolder.kt | 2 +- .../DefaultSessionControlControllerTest.kt | 27 ++++++++++++------- .../home/SessionControlInteractorTest.kt | 4 +-- 7 files changed, 54 insertions(+), 24 deletions(-) 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 d6e01c7c5..7f6aebfa7 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -211,7 +211,8 @@ class HomeFragment : Fragment() { hideOnboarding = ::hideOnboardingAndOpenSearch, registerCollectionStorageObserver = ::registerCollectionStorageObserver, showDeleteCollectionPrompt = ::showDeleteCollectionPrompt, - showTabTray = ::openTabTray + showTabTray = ::openTabTray, + handleSwipedItemDeletionCancel = ::handleSwipedItemDeletionCancel ) ) updateLayout(view) @@ -557,12 +558,21 @@ class HomeFragment : Fragment() { } } - private fun showDeleteCollectionPrompt(tabCollection: TabCollection, title: String?, message: String) { + private fun showDeleteCollectionPrompt( + tabCollection: TabCollection, + title: String?, + message: String, + wasSwiped: Boolean, + handleSwipedItemDeletionCancel: () -> Unit + ) { val context = context ?: return AlertDialog.Builder(context).apply { setTitle(title) setMessage(message) setNegativeButton(R.string.tab_collection_dialog_negative) { dialog: DialogInterface, _ -> + if (wasSwiped) { + handleSwipedItemDeletionCancel() + } dialog.cancel() } setPositiveButton(R.string.tab_collection_dialog_positive) { dialog: DialogInterface, _ -> @@ -951,6 +961,10 @@ class HomeFragment : Fragment() { view?.add_tabs_to_collections_button?.isVisible = tabCount > 0 } + private fun handleSwipedItemDeletionCancel() { + view?.sessionControlRecyclerView?.adapter?.notifyDataSetChanged() + } + companion object { const val ALL_NORMAL_TABS = "all_normal" const val ALL_PRIVATE_TABS = "all_private" diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt index 85c267842..c9df30ee2 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt @@ -63,7 +63,7 @@ interface SessionControlController { /** * @see [CollectionInteractor.onCollectionRemoveTab] */ - fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab) + fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab, wasSwiped: Boolean) /** * @see [CollectionInteractor.onCollectionShareTabsClicked] @@ -160,8 +160,15 @@ class DefaultSessionControlController( private val viewLifecycleScope: CoroutineScope, private val hideOnboarding: () -> Unit, private val registerCollectionStorageObserver: () -> Unit, - private val showDeleteCollectionPrompt: (tabCollection: TabCollection, title: String?, message: String) -> Unit, - private val showTabTray: () -> Unit + private val showDeleteCollectionPrompt: ( + tabCollection: TabCollection, + title: String?, + message: String, + wasSwiped: Boolean, + handleSwipedItemDeletionCancel: () -> Unit + ) -> Unit, + private val showTabTray: () -> Unit, + private val handleSwipedItemDeletionCancel: () -> Unit ) : SessionControlController { override fun handleCollectionAddTabTapped(collection: TabCollection) { @@ -206,7 +213,7 @@ class DefaultSessionControlController( metrics.track(Event.CollectionAllTabsRestored) } - override fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab) { + override fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab, wasSwiped: Boolean) { metrics.track(Event.CollectionTabRemoved) if (collection.tabs.size == 1) { @@ -216,7 +223,7 @@ class DefaultSessionControlController( ) val message = activity.resources.getString(R.string.delete_tab_and_collection_dialog_message) - showDeleteCollectionPrompt(collection, title, message) + showDeleteCollectionPrompt(collection, title, message, wasSwiped, handleSwipedItemDeletionCancel) } else { viewLifecycleScope.launch(Dispatchers.IO) { tabCollectionStorage.removeTabFromCollection(collection, tab) @@ -232,7 +239,7 @@ class DefaultSessionControlController( override fun handleDeleteCollectionTapped(collection: TabCollection) { val message = activity.resources.getString(R.string.tab_collection_dialog_message, collection.title) - showDeleteCollectionPrompt(collection, null, message) + showDeleteCollectionPrompt(collection, null, message, false, handleSwipedItemDeletionCancel) } override fun handleOpenInPrivateTabClicked(topSite: TopSite) { diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt index f89881d4f..644178f38 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -54,7 +54,7 @@ interface CollectionInteractor { * @param collection The collection of tabs that will be modified. * @param tab The tab to remove from the tab collection. */ - fun onCollectionRemoveTab(collection: TabCollection, tab: Tab) + fun onCollectionRemoveTab(collection: TabCollection, tab: Tab, wasSwiped: Boolean) /** * Shares the tabs in the given tab collection. Called when a user clicks on the Collection @@ -189,8 +189,8 @@ class SessionControlInteractor( controller.handleCollectionOpenTabsTapped(collection) } - override fun onCollectionRemoveTab(collection: TabCollection, tab: Tab) { - controller.handleCollectionRemoveTab(collection, tab) + override fun onCollectionRemoveTab(collection: TabCollection, tab: Tab, wasSwiped: Boolean) { + controller.handleCollectionRemoveTab(collection, tab, wasSwiped) } override fun onCollectionShareTabsClicked(collection: TabCollection) { diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt index 990fca0c1..8c488cfd6 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt @@ -29,7 +29,7 @@ class SwipeToDeleteCallback( override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { when (viewHolder) { is TabInCollectionViewHolder -> { - interactor.onCollectionRemoveTab(viewHolder.collection, viewHolder.tab) + interactor.onCollectionRemoveTab(viewHolder.collection, viewHolder.tab, wasSwiped = true) } } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt index 7f374285d..9fbb41e71 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt @@ -53,7 +53,7 @@ class TabInCollectionViewHolder( list_item_action_button.increaseTapArea(buttonIncreaseDps) list_item_action_button.setOnClickListener { - interactor.onCollectionRemoveTab(collection, tab) + interactor.onCollectionRemoveTab(collection, tab, wasSwiped = false) } } diff --git a/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt index dc1ea1e96..201515011 100644 --- a/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt @@ -32,7 +32,6 @@ import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.searchEngineManager import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController @@ -55,12 +54,17 @@ class DefaultSessionControlControllerTest { private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true) private val topSiteStorage: TopSiteStorage = mockk(relaxed = true) private val tabsUseCases: TabsUseCases = mockk(relaxed = true) - private val hideOnboarding: () -> Unit = mockk(relaxed = true) private val registerCollectionStorageObserver: () -> Unit = mockk(relaxed = true) private val showTabTray: () -> Unit = mockk(relaxed = true) - private val showDeleteCollectionPrompt: (tabCollection: TabCollection, title: String?, message: String) -> Unit = - mockk(relaxed = true) + private val handleSwipedItemDeletionCancel: () -> Unit = mockk(relaxed = true) + private val showDeleteCollectionPrompt: ( + tabCollection: TabCollection, + title: String?, + message: String, + wasSwiped: Boolean, + handleSwipedItemDeletionCancel: () -> Unit + ) -> Unit = mockk(relaxed = true) private val searchEngine = mockk(relaxed = true) private val searchEngineManager = mockk(relaxed = true) private val settings: Settings = mockk(relaxed = true) @@ -102,7 +106,8 @@ class DefaultSessionControlControllerTest { hideOnboarding = hideOnboarding, registerCollectionStorageObserver = registerCollectionStorageObserver, showDeleteCollectionPrompt = showDeleteCollectionPrompt, - showTabTray = showTabTray + showTabTray = showTabTray, + handleSwipedItemDeletionCancel = handleSwipedItemDeletionCancel ) } @@ -181,14 +186,16 @@ class DefaultSessionControlControllerTest { activity.resources.getString(R.string.delete_tab_and_collection_dialog_message) } returns "Deleting this tab will delete everything." - controller.handleCollectionRemoveTab(collection, tab) + controller.handleCollectionRemoveTab(collection, tab, false) verify { metrics.track(Event.CollectionTabRemoved) } verify { showDeleteCollectionPrompt( collection, "Delete Collection?", - "Deleting this tab will delete everything." + "Deleting this tab will delete everything.", + false, + handleSwipedItemDeletionCancel ) } } @@ -197,7 +204,7 @@ class DefaultSessionControlControllerTest { fun `handleCollectionRemoveTab multiple tabs`() { val collection: TabCollection = mockk(relaxed = true) val tab: ComponentTab = mockk(relaxed = true) - controller.handleCollectionRemoveTab(collection, tab) + controller.handleCollectionRemoveTab(collection, tab, false) verify { metrics.track(Event.CollectionTabRemoved) } } @@ -231,7 +238,9 @@ class DefaultSessionControlControllerTest { showDeleteCollectionPrompt( collection, null, - "Are you sure you want to delete Collection?" + "Are you sure you want to delete Collection?", + false, + handleSwipedItemDeletionCancel ) } } diff --git a/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt index c51bd81d6..63bc74661 100644 --- a/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt @@ -49,8 +49,8 @@ class SessionControlInteractorTest { fun onCollectionRemoveTab() { val collection: TabCollection = mockk(relaxed = true) val tab: Tab = mockk(relaxed = true) - interactor.onCollectionRemoveTab(collection, tab) - verify { controller.handleCollectionRemoveTab(collection, tab) } + interactor.onCollectionRemoveTab(collection, tab, false) + verify { controller.handleCollectionRemoveTab(collection, tab, false) } } @Test From 0f0aee5e9749f0941ca896f3dc86b2ff35c5c6b8 Mon Sep 17 00:00:00 2001 From: ekager Date: Thu, 30 Jul 2020 16:46:08 -0400 Subject: [PATCH 44/74] For #6846 - Don't show ToolbarPopupWindow if nothing to show --- .../main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt b/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt index 9e7b41204..af5f9169c 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt @@ -32,8 +32,11 @@ object ToolbarPopupWindow { copyVisible: Boolean = true ) { val context = view.get()?.context ?: return - val isCustomTabSession = customTabSession != null val clipboard = context.components.clipboardHandler + if (!copyVisible && clipboard.text.isNullOrEmpty()) return + + val isCustomTabSession = customTabSession != null + val customView = LayoutInflater.from(context) .inflate(R.layout.browser_toolbar_popup_window, null) val popupWindow = PopupWindow( From ab2ea8e682483181bb88126583f249aba2e72ee0 Mon Sep 17 00:00:00 2001 From: Mugurell Date: Fri, 31 Jul 2020 18:04:15 +0300 Subject: [PATCH 45/74] For #13037 - Use email to sign in to fxa if device has no camera App can be installed on devices with no camera modules. Like Android TV boxes. Will skip presenting the option to sign in by scanning a qr code in this case and default to login with email and password. --- .../fenix/settings/SettingsFragment.kt | 12 ++++++- .../logins/SyncLoginsPreferenceView.kt | 24 +++++++++++-- .../fragment/SavedLoginsAuthFragment.kt | 4 ++- .../logins/SyncLoginsPreferenceViewTest.kt | 36 +++++++++++++++++-- 4 files changed, 70 insertions(+), 6 deletions(-) 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 c19d5bc71..469c5397d 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -25,6 +25,7 @@ import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.Profile +import mozilla.components.support.ktx.android.content.hasCamera import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity @@ -188,7 +189,16 @@ class SettingsFragment : PreferenceFragmentCompat() { val directions: NavDirections? = when (preference.key) { resources.getString(R.string.pref_key_sign_in) -> { - SettingsFragmentDirections.actionSettingsFragmentToTurnOnSyncFragment() + // App can be installed on devices with no camera modules. Like Android TV boxes. + // Let's skip presenting the option to sign in by scanning a qr code in this case + // and default to login with email and password. + if (requireContext().hasCamera()) { + SettingsFragmentDirections.actionSettingsFragmentToTurnOnSyncFragment() + } else { + requireComponents.services.accountsAuthFeature.beginAuthentication(requireContext()) + requireComponents.analytics.metrics.track(Event.SyncAuthUseEmail) + null + } } resources.getString(R.string.pref_key_search_settings) -> { SettingsFragmentDirections.actionSettingsFragmentToSearchEngineFragment() diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceView.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceView.kt index 49e313b57..81d742d39 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceView.kt @@ -4,16 +4,21 @@ package org.mozilla.fenix.settings.logins +import android.content.Context import androidx.lifecycle.LifecycleOwner import androidx.navigation.NavController import androidx.preference.Preference import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount +import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature import mozilla.components.service.fxa.SyncEngine import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.SyncEnginesStorage +import mozilla.components.support.ktx.android.content.hasCamera import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections /** @@ -23,7 +28,9 @@ class SyncLoginsPreferenceView( private val syncLoginsPreference: Preference, lifecycleOwner: LifecycleOwner, accountManager: FxaAccountManager, - private val navController: NavController + private val navController: NavController, + private val accountsAuthFeature: FirefoxAccountsAuthFeature, + private val metrics: MetricController ) { init { @@ -68,7 +75,15 @@ class SyncLoginsPreferenceView( syncLoginsPreference.apply { summary = context.getString(R.string.preferences_passwords_sync_logins_sign_in) setOnPreferenceClickListener { - navigateToTurnOnSyncFragment() + // App can be installed on devices with no camera modules. Like Android TV boxes. + // Let's skip presenting the option to sign in by scanning a qr code in this case + // and default to login with email and password. + if (context.hasCamera()) { + navigateToTurnOnSyncFragment() + } else { + navigateToPairWithEmail(context) + } + true } } @@ -102,4 +117,9 @@ class SyncLoginsPreferenceView( val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToTurnOnSyncFragment() navController.navigate(directions) } + + private fun navigateToPairWithEmail(context: Context) { + accountsAuthFeature.beginAuthentication(context) + metrics.track(Event.SyncAuthUseEmail) + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt index 4246beccd..a7493f99b 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt @@ -145,7 +145,9 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() { requirePreference(R.string.pref_key_password_sync_logins), lifecycleOwner = viewLifecycleOwner, accountManager = requireComponents.backgroundServices.accountManager, - navController = findNavController() + navController = findNavController(), + accountsAuthFeature = requireComponents.services.accountsAuthFeature, + metrics = requireComponents.analytics.metrics ) togglePrefsEnabledWhileAuthenticating(enabled = true) diff --git a/app/src/test/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceViewTest.kt b/app/src/test/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceViewTest.kt index a6b68ecb4..fb2de9d7b 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceViewTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceViewTest.kt @@ -12,18 +12,24 @@ import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk import io.mockk.mockkConstructor +import io.mockk.mockkStatic import io.mockk.slot import io.mockk.unmockkConstructor +import io.mockk.unmockkStatic import io.mockk.verify import mozilla.components.concept.sync.AccountObserver +import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature import mozilla.components.service.fxa.SyncEngine import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.SyncEnginesStorage +import mozilla.components.support.ktx.android.content.hasCamera import org.junit.After import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections class SyncLoginsPreferenceViewTest { @@ -32,6 +38,8 @@ class SyncLoginsPreferenceViewTest { @MockK private lateinit var lifecycleOwner: LifecycleOwner @MockK private lateinit var accountManager: FxaAccountManager @MockK(relaxed = true) private lateinit var navController: NavController + @MockK(relaxed = true) private lateinit var accountsAuthFeature: FirefoxAccountsAuthFeature + @MockK(relaxed = true) private lateinit var metrics: MetricController private lateinit var accountObserver: CapturingSlot private lateinit var clickListener: CapturingSlot @@ -87,9 +95,11 @@ class SyncLoginsPreferenceViewTest { } @Test - fun `needs login if account does not exist`() { + fun `needs login if account does not exist and device has camera`() { every { accountManager.authenticatedAccount() } returns null every { accountManager.accountNeedsReauth() } returns false + mockkStatic("mozilla.components.support.ktx.android.content.ContextKt") + every { any().hasCamera() } returns true createView() verify { syncLoginsPreference.summary = "Sign in to Sync" } @@ -100,6 +110,26 @@ class SyncLoginsPreferenceViewTest { SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToTurnOnSyncFragment() ) } + + unmockkStatic("mozilla.components.support.ktx.android.content.ContextKt") + } + + @Test + fun `needs login if account does not exist and device does not have camera`() { + every { accountManager.authenticatedAccount() } returns null + every { accountManager.accountNeedsReauth() } returns false + createView() + mockkStatic("mozilla.components.support.ktx.android.content.ContextKt") + every { any().hasCamera() } returns false + + verify { syncLoginsPreference.summary = "Sign in to Sync" } + assertTrue(clickListener.captured.onPreferenceClick(syncLoginsPreference)) + verify { + accountsAuthFeature.beginAuthentication(any()) + metrics.track(Event.SyncAuthUseEmail) + } + + unmockkStatic("mozilla.components.support.ktx.android.content.ContextKt") } @Test @@ -141,6 +171,8 @@ class SyncLoginsPreferenceViewTest { syncLoginsPreference, lifecycleOwner, accountManager, - navController + navController, + accountsAuthFeature, + metrics ) } From 78126122adc29ded5b796c9b5dd88d19d9529686 Mon Sep 17 00:00:00 2001 From: Kadeem Date: Thu, 30 Jul 2020 16:48:48 -0400 Subject: [PATCH 46/74] For #12769: Refactored Background Service Test into multiple test cases. --- .../components/BackgroundServicesTest.kt | 149 +++++++++--------- 1 file changed, 74 insertions(+), 75 deletions(-) diff --git a/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt b/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt index ff9af2fe6..e0e93dad0 100644 --- a/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt @@ -4,8 +4,10 @@ package org.mozilla.fenix.components +import io.mockk.Called import io.mockk.MockKAnnotations import io.mockk.Runs +import io.mockk.confirmVerified import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just @@ -23,8 +25,11 @@ import org.mozilla.fenix.utils.Settings class BackgroundServicesTest { - @MockK private lateinit var metrics: MetricController - @MockK private lateinit var settings: Settings + @MockK + private lateinit var metrics: MetricController + + @MockK + private lateinit var settings: Settings private lateinit var observer: TelemetryAccountObserver private lateinit var registry: ObserverRegistry @@ -35,101 +40,95 @@ class BackgroundServicesTest { every { metrics.track(any()) } just Runs every { settings.fxaSignedIn = any() } just Runs - observer = TelemetryAccountObserver(mockk(relaxed = true), metrics) + observer = TelemetryAccountObserver(settings, metrics) registry = ObserverRegistry().apply { register(observer) } } @Test - fun `telemetry account observer`() { + fun `telemetry account observer tracks sign in event`() { val account = mockk() - // Sign-in registry.notifyObservers { onAuthenticated(account, AuthType.Signin) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignUp)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthPaired)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthFromShared)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthRecovered)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthOtherExternal)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + verify { metrics.track(Event.SyncAuthSignIn) } + verify { settings.fxaSignedIn = true } + confirmVerified(metrics, settings) + } + + @Test + fun `telemetry account observer tracks sign up event`() { + val account = mockk() - // Sign-up registry.notifyObservers { onAuthenticated(account, AuthType.Signup) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthPaired)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthFromShared)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthRecovered)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthOtherExternal)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + verify { metrics.track(Event.SyncAuthSignUp) } + verify { settings.fxaSignedIn = true } + confirmVerified(metrics, settings) + } + + @Test + fun `telemetry account observer tracks pairing event`() { + val account = mockk() - // Pairing registry.notifyObservers { onAuthenticated(account, AuthType.Pairing) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthFromShared)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthRecovered)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthOtherExternal)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + verify { metrics.track(Event.SyncAuthPaired) } + verify { settings.fxaSignedIn = true } + confirmVerified(metrics, settings) + } + + @Test + fun `telemetry account observer tracks shared event`() { + val account = mockk() - // Auto-login/shared account registry.notifyObservers { onAuthenticated(account, AuthType.Shared) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthFromShared)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthRecovered)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthOtherExternal)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + verify { metrics.track(Event.SyncAuthFromShared) } + verify { settings.fxaSignedIn = true } + confirmVerified(metrics, settings) + } + + @Test + fun `telemetry account observer tracks recovered event`() { + val account = mockk() - // Internally recovered registry.notifyObservers { onAuthenticated(account, AuthType.Recovered) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthFromShared)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthRecovered)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthOtherExternal)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + verify { metrics.track(Event.SyncAuthRecovered) } + verify { settings.fxaSignedIn = true } + confirmVerified(metrics, settings) + } + + @Test + fun `telemetry account observer tracks external creation event with null action`() { + val account = mockk() - // Other external registry.notifyObservers { onAuthenticated(account, AuthType.OtherExternal(null)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthFromShared)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthRecovered)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthOtherExternal)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + verify { metrics.track(Event.SyncAuthOtherExternal) } + verify { settings.fxaSignedIn = true } + confirmVerified(metrics, settings) + } + + @Test + fun `telemetry account observer tracks external creation event with some action`() { + val account = mockk() registry.notifyObservers { onAuthenticated(account, AuthType.OtherExternal("someAction")) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthFromShared)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthRecovered)) } - verify(exactly = 2) { metrics.track(eq(Event.SyncAuthOtherExternal)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + verify { metrics.track(Event.SyncAuthOtherExternal) } + verify { settings.fxaSignedIn = true } + confirmVerified(metrics, settings) + } + + @Test + fun `telemetry account observer does not track existing account`() { + val account = mockk() - // NB: 'Existing' auth type isn't expected to record any auth telemetry. registry.notifyObservers { onAuthenticated(account, AuthType.Existing) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthFromShared)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthRecovered)) } - verify(exactly = 2) { metrics.track(eq(Event.SyncAuthOtherExternal)) } - verify(exactly = 0) { metrics.track(eq(Event.SyncAuthSignOut)) } + verify { metrics wasNot Called } + verify { settings.fxaSignedIn = true } + confirmVerified(metrics, settings) + } - // Logout + @Test + fun `telemetry account observer tracks sign out event`() { registry.notifyObservers { onLoggedOut() } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignIn)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignUp)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthPaired)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthFromShared)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthRecovered)) } - verify(exactly = 2) { metrics.track(eq(Event.SyncAuthOtherExternal)) } - verify(exactly = 1) { metrics.track(eq(Event.SyncAuthSignOut)) } + verify { metrics.track(Event.SyncAuthSignOut) } + verify { settings.fxaSignedIn = false } + confirmVerified(metrics, settings) } } From a11277079ac7e3dfde589f8dc6914ae99fcd5140 Mon Sep 17 00:00:00 2001 From: liuche Date: Fri, 31 Jul 2020 10:54:01 -0700 Subject: [PATCH 47/74] Update README.md (#13150) We'll be redirecting people from bugzilla, and we've started Fennec rollout, so no time like the present to update references in the readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 19ab254c7..a9c954305 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# Firefox Preview +# Firefox for Android [![Task Status](https://github.taskcluster.net/v1/repository/mozilla-mobile/fenix/master/badge.svg)](https://github.taskcluster.net/v1/repository/mozilla-mobile/fenix/master/latest) [![codecov](https://codecov.io/gh/mozilla-mobile/fenix/branch/master/graph/badge.svg)](https://codecov.io/gh/mozilla-mobile/fenix) -Firefox Preview (internal code name: "Fenix") is an all-new browser for Android, based on [GeckoView](https://mozilla.github.io/geckoview/) and [Mozilla Android Components](https://mozac.org/). +Fenix (internal codename) is the all-new Firefox for Android browser, based on [GeckoView](https://mozilla.github.io/geckoview/) and [Mozilla Android Components](https://mozac.org/). ** Note: The team is currently experiencing heavy triage and review load, so when triaging issues, we will mainly be looking to identify [S1 (high severity)](https://github.com/mozilla-mobile/fenix/labels/S1) issues. See our triage process [here](https://github.com/mozilla-mobile/fenix/wiki/Triage-Process). Please be patient if you don't hear back from us immediately on your issue! ** From bfc955cd402d707862609b2a4f47722c4f373d46 Mon Sep 17 00:00:00 2001 From: Mihai Branescu Date: Tue, 16 Jun 2020 10:42:16 +0300 Subject: [PATCH 48/74] For #11498 - add Sync tabs error view (including sign-in CTA) --- .../mozilla/fenix/sync/SyncedTabsAdapter.kt | 11 +++- .../mozilla/fenix/sync/SyncedTabsLayout.kt | 4 +- .../fenix/sync/SyncedTabsViewHolder.kt | 60 ++++++++++++++++++- .../main/res/layout/component_sync_tabs.xml | 14 ----- app/src/main/res/values/strings.xml | 2 - 5 files changed, 72 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt index 69ace066f..2d3e8b885 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt @@ -6,10 +6,13 @@ package org.mozilla.fenix.sync import android.view.LayoutInflater import android.view.ViewGroup +import androidx.navigation.NavController import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import mozilla.components.browser.storage.sync.SyncedDeviceTabs import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder +import org.mozilla.fenix.sync.SyncedTabsViewHolder.ErrorViewHolder +import org.mozilla.fenix.sync.SyncedTabsViewHolder.SignInViewHolder import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder import mozilla.components.browser.storage.sync.Tab as SyncTab import mozilla.components.concept.sync.Device as SyncDevice @@ -24,6 +27,8 @@ class SyncedTabsAdapter( return when (viewType) { DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView) TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView) + ErrorViewHolder.LAYOUT_ID -> ErrorViewHolder(itemView) + SignInViewHolder.LAYOUT_ID -> SignInViewHolder(itemView) else -> throw IllegalStateException() } } @@ -35,6 +40,8 @@ class SyncedTabsAdapter( override fun getItemViewType(position: Int) = when (getItem(position)) { is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID + is AdapterItem.Error -> ErrorViewHolder.LAYOUT_ID + is AdapterItem.SignIn -> SignInViewHolder.LAYOUT_ID } fun updateData(syncedTabs: List) { @@ -55,7 +62,7 @@ class SyncedTabsAdapter( when (oldItem) { is AdapterItem.Device -> newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id - is AdapterItem.Tab -> + is AdapterItem.Tab, AdapterItem.Error, AdapterItem.SignIn -> oldItem == newItem } @@ -67,5 +74,7 @@ class SyncedTabsAdapter( sealed class AdapterItem { data class Device(val device: SyncDevice) : AdapterItem() data class Tab(val tab: SyncTab) : AdapterItem() + data class SignIn(val navController: NavController) : AdapterItem() + data class Error(val errorResId: Int) : AdapterItem() } } diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt index 0889ccb50..c1decbf0f 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt @@ -6,8 +6,10 @@ package org.mozilla.fenix.sync import android.content.Context import android.util.AttributeSet -import android.view.View import android.widget.FrameLayout +import androidx.annotation.StringRes +import androidx.fragment.app.findFragment +import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.component_sync_tabs.view.* import kotlinx.coroutines.CoroutineScope diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt index bcd494cf0..8be6c56d1 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt @@ -5,11 +5,17 @@ package org.mozilla.fenix.sync import android.view.View +import android.view.View.GONE +import android.widget.LinearLayout +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.no_content_message_with_action.view.* import kotlinx.android.synthetic.main.sync_tabs_list_item.view.* import kotlinx.android.synthetic.main.view_synced_tabs_group.view.* import mozilla.components.browser.storage.sync.Tab import mozilla.components.concept.sync.DeviceType +import mozilla.components.support.ktx.android.util.dpToPx +import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem @@ -38,6 +44,44 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item } } + class SignInViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { + + override fun bind(item: T, interactor: (Tab) -> Unit) { + val signInItem = item as AdapterItem.SignIn + setErrorMargins() + + itemView.no_content_header.visibility = GONE + itemView.no_content_description.text = + itemView.context.getString(R.string.synced_tabs_sign_in_message) + itemView.no_content_button.text = + itemView.context.getString(R.string.synced_tabs_sign_in_button) + itemView.no_content_button.icon = + ContextCompat.getDrawable(itemView.context, R.drawable.ic_sign_in) + itemView.no_content_button.setOnClickListener { + signInItem.navController.navigate(NavGraphDirections.actionGlobalTurnOnSync()) + } + } + + companion object { + const val LAYOUT_ID = R.layout.no_content_message_with_action + } + } + + class ErrorViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { + + override fun bind(item: T, interactor: (Tab) -> Unit) { + val errorItem = item as AdapterItem.Error + setErrorMargins() + + itemView.no_content_header.visibility = GONE + itemView.no_content_description.text = itemView.context.getString(errorItem.errorResId) + } + + companion object { + const val LAYOUT_ID = R.layout.no_content_message + } + } + class DeviceViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { override fun bind(item: T, interactor: (Tab) -> Unit) { @@ -45,7 +89,6 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item } private fun bindHeader(device: AdapterItem.Device) { - val deviceLogoDrawable = when (device.device.deviceType) { DeviceType.DESKTOP -> R.drawable.mozac_ic_device_desktop else -> R.drawable.mozac_ic_device_mobile @@ -59,4 +102,19 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item const val LAYOUT_ID = R.layout.view_synced_tabs_group } } + + internal fun setErrorMargins() { + val lp = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + val displayMetrics = itemView.context.resources.displayMetrics + val margin = ERROR_MARGIN.dpToPx(displayMetrics) + lp.setMargins(margin, margin, margin, 0) + itemView.layoutParams = lp + } + + companion object { + private const val ERROR_MARGIN = 20 + } } diff --git a/app/src/main/res/layout/component_sync_tabs.xml b/app/src/main/res/layout/component_sync_tabs.xml index 0ec378aad..f5a07f096 100644 --- a/app/src/main/res/layout/component_sync_tabs.xml +++ b/app/src/main/res/layout/component_sync_tabs.xml @@ -20,20 +20,6 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" /> - - A login with that username already exists - - Connect with a Firefox Account. Connect another device. From 5d1aeb5ea760c2cb6a0a914118e9e75a6839f13a Mon Sep 17 00:00:00 2001 From: Mihai Branescu Date: Tue, 16 Jun 2020 11:41:15 +0300 Subject: [PATCH 49/74] For #11499 - replaced string for no tabs available --- app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt | 2 +- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt index c1decbf0f..16288aa15 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt @@ -96,7 +96,7 @@ class SyncedTabsLayout @JvmOverloads constructor( internal fun stringResourceForError(error: SyncedTabsView.ErrorType) = when (error) { SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing - SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_connect_to_sync_account + SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_sign_in_message SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0394da6d6..2e71813b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1432,7 +1432,7 @@ A login with that username already exists - + Connect another device. Please re-authenticate. From ff50dae8e94b75fed61147048dc72f6409ffbd57 Mon Sep 17 00:00:00 2001 From: Mihai Branescu Date: Thu, 25 Jun 2020 10:49:35 +0300 Subject: [PATCH 50/74] For #11498 - removed SignIn ViewHolder and unified with the Error one --- .../mozilla/fenix/sync/SyncedTabsAdapter.kt | 17 ++++--- .../mozilla/fenix/sync/SyncedTabsLayout.kt | 40 +++++++++++----- .../fenix/sync/SyncedTabsViewHolder.kt | 48 ++++++++----------- .../main/res/layout/sync_tabs_error_row.xml | 31 ++++++++++++ app/src/main/res/values/strings.xml | 2 + 5 files changed, 89 insertions(+), 49 deletions(-) create mode 100644 app/src/main/res/layout/sync_tabs_error_row.xml diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt index 2d3e8b885..e51419df4 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt @@ -12,7 +12,6 @@ import androidx.recyclerview.widget.ListAdapter import mozilla.components.browser.storage.sync.SyncedDeviceTabs import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder import org.mozilla.fenix.sync.SyncedTabsViewHolder.ErrorViewHolder -import org.mozilla.fenix.sync.SyncedTabsViewHolder.SignInViewHolder import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder import mozilla.components.browser.storage.sync.Tab as SyncTab import mozilla.components.concept.sync.Device as SyncDevice @@ -28,7 +27,6 @@ class SyncedTabsAdapter( DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView) TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView) ErrorViewHolder.LAYOUT_ID -> ErrorViewHolder(itemView) - SignInViewHolder.LAYOUT_ID -> SignInViewHolder(itemView) else -> throw IllegalStateException() } } @@ -38,10 +36,9 @@ class SyncedTabsAdapter( } override fun getItemViewType(position: Int) = when (getItem(position)) { - is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID - is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID - is AdapterItem.Error -> ErrorViewHolder.LAYOUT_ID - is AdapterItem.SignIn -> SignInViewHolder.LAYOUT_ID + is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID + is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID + is AdapterItem.Error -> ErrorViewHolder.LAYOUT_ID } fun updateData(syncedTabs: List) { @@ -62,7 +59,7 @@ class SyncedTabsAdapter( when (oldItem) { is AdapterItem.Device -> newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id - is AdapterItem.Tab, AdapterItem.Error, AdapterItem.SignIn -> + is AdapterItem.Tab, is AdapterItem.Error -> oldItem == newItem } @@ -74,7 +71,9 @@ class SyncedTabsAdapter( sealed class AdapterItem { data class Device(val device: SyncDevice) : AdapterItem() data class Tab(val tab: SyncTab) : AdapterItem() - data class SignIn(val navController: NavController) : AdapterItem() - data class Error(val errorResId: Int) : AdapterItem() + data class Error( + val descriptionResId: Int, + val navController: NavController? = null + ) : AdapterItem() } } diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt index 16288aa15..564efb63f 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt @@ -9,6 +9,7 @@ import android.util.AttributeSet import android.widget.FrameLayout import androidx.annotation.StringRes import androidx.fragment.app.findFragment +import androidx.navigation.NavController import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.component_sync_tabs.view.* @@ -19,6 +20,7 @@ import kotlinx.coroutines.launch import mozilla.components.browser.storage.sync.SyncedDeviceTabs import mozilla.components.feature.syncedtabs.view.SyncedTabsView import org.mozilla.fenix.R +import java.lang.IllegalStateException class SyncedTabsLayout @JvmOverloads constructor( context: Context, @@ -45,10 +47,17 @@ class SyncedTabsLayout @JvmOverloads constructor( // We may still be displaying a "loading" spinner, hide it. stopLoading() - sync_tabs_status.text = context.getText(stringResourceForError(error)) + val navController: NavController? = try { + findFragment().findNavController() + } catch (exception: IllegalStateException) { + null + } - synced_tabs_list.visibility = View.GONE - sync_tabs_status.visibility = View.VISIBLE + val descriptionResId = stringResourceForError(error) + val errorItem = getErrorItem(navController, error, descriptionResId) + + val errorList: List = listOf(errorItem) + adapter.submitList(errorList) synced_tabs_pull_to_refresh.isEnabled = pullToRefreshEnableState(error) } @@ -56,17 +65,11 @@ class SyncedTabsLayout @JvmOverloads constructor( override fun displaySyncedTabs(syncedTabs: List) { coroutineScope.launch { - synced_tabs_list.visibility = View.VISIBLE - sync_tabs_status.visibility = View.GONE - adapter.updateData(syncedTabs) } } override fun startLoading() { - synced_tabs_list.visibility = View.VISIBLE - sync_tabs_status.visibility = View.GONE - synced_tabs_pull_to_refresh.isRefreshing = true } @@ -80,7 +83,8 @@ class SyncedTabsLayout @JvmOverloads constructor( } companion object { - internal fun pullToRefreshEnableState(error: SyncedTabsView.ErrorType) = when (error) { + + private fun pullToRefreshEnableState(error: SyncedTabsView.ErrorType) = when (error) { // Disable "pull-to-refresh" when we clearly can't sync tabs, and user needs to take an // action within the app. SyncedTabsView.ErrorType.SYNC_UNAVAILABLE, @@ -93,12 +97,26 @@ class SyncedTabsLayout @JvmOverloads constructor( SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> true } - internal fun stringResourceForError(error: SyncedTabsView.ErrorType) = when (error) { + private fun stringResourceForError(error: SyncedTabsView.ErrorType) = when (error) { SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_sign_in_message SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs } + + private fun getErrorItem( + navController: NavController?, + error: SyncedTabsView.ErrorType, + @StringRes stringResId: Int + ): SyncedTabsAdapter.AdapterItem = when (error) { + SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE, + SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE, + SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION, + SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> SyncedTabsAdapter.AdapterItem + .Error(descriptionResId = stringResId) + SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> SyncedTabsAdapter.AdapterItem + .Error(descriptionResId = stringResId, navController = navController) + } } } diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt index 8be6c56d1..95abada88 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt @@ -6,10 +6,10 @@ package org.mozilla.fenix.sync import android.view.View import android.view.View.GONE +import android.view.View.VISIBLE import android.widget.LinearLayout -import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.no_content_message_with_action.view.* +import kotlinx.android.synthetic.main.sync_tabs_error_row.view.* import kotlinx.android.synthetic.main.sync_tabs_list_item.view.* import kotlinx.android.synthetic.main.view_synced_tabs_group.view.* import mozilla.components.browser.storage.sync.Tab @@ -44,41 +44,26 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item } } - class SignInViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { - - override fun bind(item: T, interactor: (Tab) -> Unit) { - val signInItem = item as AdapterItem.SignIn - setErrorMargins() - - itemView.no_content_header.visibility = GONE - itemView.no_content_description.text = - itemView.context.getString(R.string.synced_tabs_sign_in_message) - itemView.no_content_button.text = - itemView.context.getString(R.string.synced_tabs_sign_in_button) - itemView.no_content_button.icon = - ContextCompat.getDrawable(itemView.context, R.drawable.ic_sign_in) - itemView.no_content_button.setOnClickListener { - signInItem.navController.navigate(NavGraphDirections.actionGlobalTurnOnSync()) - } - } - - companion object { - const val LAYOUT_ID = R.layout.no_content_message_with_action - } - } - class ErrorViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { override fun bind(item: T, interactor: (Tab) -> Unit) { val errorItem = item as AdapterItem.Error setErrorMargins() - itemView.no_content_header.visibility = GONE - itemView.no_content_description.text = itemView.context.getString(errorItem.errorResId) + itemView.sync_tabs_error_description.text = + itemView.context.getString(errorItem.descriptionResId) + itemView.sync_tabs_error_cta_button.visibility = GONE + + errorItem.navController?.let { navController -> + itemView.sync_tabs_error_cta_button.visibility = VISIBLE + itemView.sync_tabs_error_cta_button.setOnClickListener { + navController.navigate(NavGraphDirections.actionGlobalTurnOnSync()) + } + } } companion object { - const val LAYOUT_ID = R.layout.no_content_message + const val LAYOUT_ID = R.layout.sync_tabs_error_row } } @@ -95,7 +80,12 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item } itemView.synced_tabs_group_name.text = device.device.displayName - itemView.synced_tabs_group_name.setCompoundDrawablesWithIntrinsicBounds(deviceLogoDrawable, 0, 0, 0) + itemView.synced_tabs_group_name.setCompoundDrawablesWithIntrinsicBounds( + deviceLogoDrawable, + 0, + 0, + 0 + ) } companion object { diff --git a/app/src/main/res/layout/sync_tabs_error_row.xml b/app/src/main/res/layout/sync_tabs_error_row.xml new file mode 100644 index 000000000..f9005af30 --- /dev/null +++ b/app/src/main/res/layout/sync_tabs_error_row.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e71813b0..1760b7a58 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1464,4 +1464,6 @@ Show search shortcuts Search Engine + + Connect with a Firefox Account. From 64072a12561476017cedc52d67db009bb39b8fba Mon Sep 17 00:00:00 2001 From: Mihai Branescu Date: Fri, 31 Jul 2020 20:12:53 +0300 Subject: [PATCH 51/74] For #11498/#11499 - added/fixed unit tests --- .../mozilla/fenix/sync/SyncedTabsLayout.kt | 6 +-- .../fenix/sync/SyncedTabsLayoutTest.kt | 51 ++++++++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt index 564efb63f..8308692ba 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt @@ -84,7 +84,7 @@ class SyncedTabsLayout @JvmOverloads constructor( companion object { - private fun pullToRefreshEnableState(error: SyncedTabsView.ErrorType) = when (error) { + internal fun pullToRefreshEnableState(error: SyncedTabsView.ErrorType) = when (error) { // Disable "pull-to-refresh" when we clearly can't sync tabs, and user needs to take an // action within the app. SyncedTabsView.ErrorType.SYNC_UNAVAILABLE, @@ -97,7 +97,7 @@ class SyncedTabsLayout @JvmOverloads constructor( SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> true } - private fun stringResourceForError(error: SyncedTabsView.ErrorType) = when (error) { + internal fun stringResourceForError(error: SyncedTabsView.ErrorType) = when (error) { SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_sign_in_message @@ -105,7 +105,7 @@ class SyncedTabsLayout @JvmOverloads constructor( SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs } - private fun getErrorItem( + internal fun getErrorItem( navController: NavController?, error: SyncedTabsView.ErrorType, @StringRes stringResId: Int diff --git a/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsLayoutTest.kt b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsLayoutTest.kt index 9805c4445..a04bfbb51 100644 --- a/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsLayoutTest.kt +++ b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsLayoutTest.kt @@ -4,9 +4,13 @@ package org.mozilla.fenix.sync +import androidx.navigation.NavController +import io.mockk.mockk import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test import org.mozilla.fenix.R @@ -33,7 +37,7 @@ class SyncedTabsLayoutTest { SyncedTabsLayout.stringResourceForError(ErrorType.SYNC_ENGINE_UNAVAILABLE) ) assertEquals( - R.string.synced_tabs_connect_to_sync_account, + R.string.synced_tabs_sign_in_message, SyncedTabsLayout.stringResourceForError(ErrorType.SYNC_UNAVAILABLE) ) assertEquals( @@ -45,4 +49,49 @@ class SyncedTabsLayoutTest { SyncedTabsLayout.stringResourceForError(ErrorType.NO_TABS_AVAILABLE) ) } + + @Test + fun `get error item`() { + val navController = mockk() + + var errorItem = SyncedTabsLayout.getErrorItem( + navController, + ErrorType.MULTIPLE_DEVICES_UNAVAILABLE, + R.string.synced_tabs_connect_another_device + ) + assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController) + assertEquals(R.string.synced_tabs_connect_another_device, errorItem.descriptionResId) + + errorItem = SyncedTabsLayout.getErrorItem( + navController, + ErrorType.SYNC_ENGINE_UNAVAILABLE, + R.string.synced_tabs_enable_tab_syncing + ) + assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController) + assertEquals(R.string.synced_tabs_enable_tab_syncing, errorItem.descriptionResId) + + errorItem = SyncedTabsLayout.getErrorItem( + navController, + ErrorType.SYNC_NEEDS_REAUTHENTICATION, + R.string.synced_tabs_reauth + ) + assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController) + assertEquals(R.string.synced_tabs_reauth, errorItem.descriptionResId) + + errorItem = SyncedTabsLayout.getErrorItem( + navController, + ErrorType.NO_TABS_AVAILABLE, + R.string.synced_tabs_no_tabs + ) + assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController) + assertEquals(R.string.synced_tabs_no_tabs, errorItem.descriptionResId) + + errorItem = SyncedTabsLayout.getErrorItem( + navController, + ErrorType.SYNC_UNAVAILABLE, + R.string.synced_tabs_sign_in_message + ) + assertNotNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController) + assertEquals(R.string.synced_tabs_sign_in_message, errorItem.descriptionResId) + } } From 52e19ec743f1dcaa5b734607871fd2b68d490852 Mon Sep 17 00:00:00 2001 From: Kainalu Hagiwara Date: Thu, 30 Jul 2020 17:16:40 -0700 Subject: [PATCH 52/74] For #12888 - Add highlight to current page in tab history. --- .../fenix/tabhistory/TabHistoryViewHolder.kt | 13 +++++----- app/src/main/res/values-night/colors.xml | 3 +++ app/src/main/res/values/attrs.xml | 3 +++ app/src/main/res/values/colors.xml | 12 +++++++++ app/src/main/res/values/styles.xml | 6 +++++ .../fenix/tabhistory/TabHistoryAdapterTest.kt | 26 ++++++++++++------- 6 files changed, 47 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt index cfa2cc81a..6366dca52 100644 --- a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt @@ -5,10 +5,10 @@ package org.mozilla.fenix.tabhistory import android.view.View -import androidx.core.text.bold -import androidx.core.text.buildSpannedString import androidx.core.view.isVisible import kotlinx.android.synthetic.main.tab_history_list_item.* +import mozilla.components.support.ktx.android.content.getColorFromAttr +import org.mozilla.fenix.R import org.mozilla.fenix.library.LibrarySiteItemView import org.mozilla.fenix.utils.view.ViewHolder @@ -28,15 +28,14 @@ class TabHistoryViewHolder( history_layout.displayAs(LibrarySiteItemView.ItemType.SITE) history_layout.overflowView.isVisible = false + history_layout.titleView.text = item.title history_layout.urlView.text = item.url history_layout.loadFavicon(item.url) - history_layout.titleView.text = if (item.isSelected) { - buildSpannedString { - bold { append(item.title) } - } + if (item.isSelected) { + history_layout.setBackgroundColor(history_layout.context.getColorFromAttr(R.attr.tabHistoryItemSelectedBackground)) } else { - item.title + history_layout.background = null } } } diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 5ff68a52a..29193af41 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -67,6 +67,9 @@ @color/tab_tray_item_thumbnail_icon_dark_theme @color/tab_tray_selected_mask_dark_theme + + @color/tab_tray_item_selected_background_dark_theme + @color/top_site_title_text_dark_theme diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 35eb091d7..292f24ea5 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -66,6 +66,9 @@ + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 4e7101f93..3dbeaf224 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -77,6 +77,9 @@ @color/photonLightGrey60 @color/violet_70_12a + + @color/tab_tray_item_selected_background_light_theme + #FBFBFE #A7A2B7 @@ -140,6 +143,9 @@ @color/photonDarkGrey05 @color/violet_50_32a + + @color/tab_tray_item_selected_background_dark_theme + #FBFBFE #A7A2B7 @@ -195,6 +201,9 @@ @color/violet_50 @color/violet_50_48a + + @color/tab_tray_item_selected_background_dark_theme + @color/primary_text_light_theme @color/secondary_text_light_theme @@ -254,6 +263,9 @@ @color/tab_tray_item_thumbnail_icon_light_theme @color/tab_tray_selected_mask_light_theme + + @color/tab_history_item_selected_background_light_theme + #DFDFE3 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b67e9349e..9b89fb6da 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -93,6 +93,9 @@ @color/tab_tray_item_thumbnail_background_normal_theme @color/tab_tray_item_thumbnail_icon_normal_theme + + @color/tab_history_item_selected_background_normal_theme + @drawable/ic_logo_wordmark_normal @color/foundation_normal_theme @@ -231,6 +234,9 @@ @color/tab_tray_item_thumbnail_background_normal_theme @color/tab_tray_item_thumbnail_icon_normal_theme + + @color/tab_history_item_selected_background_private_theme + @drawable/ic_logo_wordmark_private @drawable/private_home_background_gradient diff --git a/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryAdapterTest.kt index 72662e807..7bfd37904 100644 --- a/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryAdapterTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryAdapterTest.kt @@ -4,12 +4,14 @@ package org.mozilla.fenix.tabhistory -import android.text.Spanned +import android.content.Context +import android.graphics.drawable.ColorDrawable import android.widget.FrameLayout import androidx.appcompat.view.ContextThemeWrapper -import androidx.core.text.HtmlCompat -import io.mockk.mockk +import io.mockk.MockKAnnotations +import io.mockk.impl.annotations.MockK import kotlinx.android.synthetic.main.history_list_item.* +import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.test.robolectric.testContext import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -23,8 +25,10 @@ import org.mozilla.fenix.helpers.FenixRobolectricTestRunner @RunWith(FenixRobolectricTestRunner::class) class TabHistoryAdapterTest { - private lateinit var parent: FrameLayout + @MockK private lateinit var interactor: TabHistoryInteractor + private lateinit var context: Context + private lateinit var parent: FrameLayout private lateinit var adapter: TabHistoryAdapter private val selectedItem = TabHistoryItem( @@ -42,8 +46,9 @@ class TabHistoryAdapterTest { @Before fun setup() { - parent = FrameLayout(ContextThemeWrapper(testContext, R.style.NormalTheme)) - interactor = mockk() + MockKAnnotations.init(this) + context = ContextThemeWrapper(testContext, R.style.NormalTheme) + parent = FrameLayout(context) adapter = TabHistoryAdapter(interactor) } @@ -54,12 +59,15 @@ class TabHistoryAdapterTest { val holder = adapter.createViewHolder(parent, 0) adapter.bindViewHolder(holder, 0) - val htmlSelected = HtmlCompat.toHtml(holder.history_layout.titleView.text as Spanned, 0) - assertTrue(htmlSelected, "Mozilla" in htmlSelected) + assertEquals("Mozilla", holder.history_layout.titleView.text) + assertEquals( + context.getColorFromAttr(R.attr.tabHistoryItemSelectedBackground), + (holder.history_layout.background as ColorDrawable).color + ) adapter.bindViewHolder(holder, 1) - assertFalse(holder.history_layout.titleView.text is Spanned) assertEquals("Firefox", holder.history_layout.titleView.text) + assertEquals(null, holder.history_layout.background) } @Test From 5e38ccc5e4fb3cfb475474f2e97d53ce23315a12 Mon Sep 17 00:00:00 2001 From: prabhat3108 Date: Tue, 28 Jul 2020 18:00:10 +0530 Subject: [PATCH 53/74] swapped deleted and save icon in bookmarks_edit menu. closes#11490 --- app/src/main/res/menu/bookmarks_edit.xml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/menu/bookmarks_edit.xml b/app/src/main/res/menu/bookmarks_edit.xml index d529806c4..f9a024b3d 100644 --- a/app/src/main/res/menu/bookmarks_edit.xml +++ b/app/src/main/res/menu/bookmarks_edit.xml @@ -4,7 +4,13 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - + - - From 8b923fc7a42a1d26136229d336d2daea38c4e364 Mon Sep 17 00:00:00 2001 From: ekager Date: Fri, 31 Jul 2020 14:48:37 -0400 Subject: [PATCH 54/74] For #13135 - Replace about top header with Firefox Daylight --- app/src/main/java/org/mozilla/fenix/Config.kt | 8 +++-- .../fenix/settings/about/AboutFragment.kt | 32 +++++++++++++------ app/src/main/res/values/static_strings.xml | 3 ++ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/Config.kt b/app/src/main/java/org/mozilla/fenix/Config.kt index 1cc1ac176..52454b373 100644 --- a/app/src/main/java/org/mozilla/fenix/Config.kt +++ b/app/src/main/java/org/mozilla/fenix/Config.kt @@ -6,9 +6,7 @@ package org.mozilla.fenix enum class ReleaseChannel { FenixDebug, - FenixProduction, - FennecProduction, FennecBeta; @@ -35,6 +33,12 @@ enum class ReleaseChannel { else -> false } + val isRelease: Boolean + get() = when (this) { + FennecProduction -> true + else -> false + } + val isBeta: Boolean get() = when (this) { FennecBeta -> true diff --git a/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt index 7bda04ec7..ece022c02 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt @@ -17,6 +17,7 @@ import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import kotlinx.android.synthetic.main.fragment_about.* import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BuildConfig +import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event @@ -38,6 +39,7 @@ import org.mozilla.geckoview.BuildConfig as GeckoViewBuildConfig */ class AboutFragment : Fragment(), AboutPageListener { + private lateinit var headerAppName: String private lateinit var appName: String private val aboutPageAdapter: AboutPageAdapter = AboutPageAdapter(this) @@ -48,6 +50,8 @@ class AboutFragment : Fragment(), AboutPageListener { ): View? { val rootView = inflater.inflate(R.layout.fragment_about, container, false) appName = getString(R.string.app_name) + headerAppName = + if (Config.channel.isRelease) getString(R.string.daylight_app_name) else appName activity?.title = getString(R.string.preferences_about, appName) return rootView @@ -64,10 +68,12 @@ class AboutFragment : Fragment(), AboutPageListener { ) } - lifecycle.addObserver(SecretDebugMenuTrigger( - logoView = wordmark, - settings = view.context.settings() - )) + lifecycle.addObserver( + SecretDebugMenuTrigger( + logoView = wordmark, + settings = view.context.settings() + ) + ) populateAboutHeader() aboutPageAdapter.submitList(populateAboutList()) @@ -75,12 +81,15 @@ class AboutFragment : Fragment(), AboutPageListener { private fun populateAboutHeader() { val aboutText = try { - val packageInfo = requireContext().packageManager.getPackageInfo(requireContext().packageName, 0) + val packageInfo = + requireContext().packageManager.getPackageInfo(requireContext().packageName, 0) val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo).toString() val componentsAbbreviation = getString(R.string.components_abbreviation) - val componentsVersion = mozilla.components.Build.version + ", " + mozilla.components.Build.gitHash + val componentsVersion = + mozilla.components.Build.version + ", " + mozilla.components.Build.gitHash val maybeGecko = getString(R.string.gecko_view_abbreviation) - val geckoVersion = GeckoViewBuildConfig.MOZ_APP_VERSION + "-" + GeckoViewBuildConfig.MOZ_APP_BUILDID + val geckoVersion = + GeckoViewBuildConfig.MOZ_APP_VERSION + "-" + GeckoViewBuildConfig.MOZ_APP_BUILDID val appServicesAbbreviation = getString(R.string.app_services_abbreviation) val appServicesVersion = mozilla.components.Build.applicationServicesVersion @@ -99,7 +108,7 @@ class AboutFragment : Fragment(), AboutPageListener { "" } - val content = getString(R.string.about_content, appName) + val content = getString(R.string.about_content, headerAppName) val buildDate = BuildConfig.BUILD_DATE about_text.text = aboutText @@ -160,7 +169,12 @@ class AboutFragment : Fragment(), AboutPageListener { private fun openLibrariesPage() { startActivity(Intent(context, OssLicensesMenuActivity::class.java)) - OssLicensesMenuActivity.setActivityTitle(getString(R.string.open_source_licenses_title, appName)) + OssLicensesMenuActivity.setActivityTitle( + getString( + R.string.open_source_licenses_title, + appName + ) + ) } override fun onAboutItemClicked(item: AboutItem) { diff --git a/app/src/main/res/values/static_strings.xml b/app/src/main/res/values/static_strings.xml index 0d23cbf7c..cac5c60b5 100644 --- a/app/src/main/res/values/static_strings.xml +++ b/app/src/main/res/values/static_strings.xml @@ -36,4 +36,7 @@ link + + + Firefox Daylight From 1d28f63737150beef6252bbdf9fe3b9fe0abe0ab Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Fri, 31 Jul 2020 13:24:14 -0700 Subject: [PATCH 55/74] Closes #12522: Reuse exceptions code (#13047) --- .../java/org/mozilla/fenix/HomeActivity.kt | 2 +- .../fenix/exceptions/ExceptionsAdapter.kt | 100 ++++++++++++ .../fenix/exceptions/ExceptionsInteractor.kt | 21 +++ .../fenix/exceptions/ExceptionsView.kt | 41 +++++ .../login}/ExceptionsFragmentStore.kt | 4 +- .../login/LoginExceptionsAdapter.kt | 44 +++++ .../login}/LoginExceptionsFragment.kt | 36 ++--- .../login/LoginExceptionsInteractor.kt | 31 ++++ .../exceptions/login/LoginExceptionsView.kt | 29 ++++ .../ExceptionsFragmentStore.kt | 10 +- .../TrackingProtectionExceptionsAdapter.kt | 45 ++++++ .../TrackingProtectionExceptionsFragment.kt | 66 ++++++++ .../TrackingProtectionExceptionsInteractor.kt | 54 +++++++ .../TrackingProtectionExceptionsView.kt | 33 ++++ .../ExceptionsDeleteButtonViewHolder.kt | 14 +- .../ExceptionsHeaderViewHolder.kt} | 11 +- .../ExceptionsListItemViewHolder.kt | 42 +++++ .../loginexceptions/LoginExceptionsAdapter.kt | 83 ---------- .../LoginExceptionsInteractor.kt | 24 --- .../loginexceptions/LoginExceptionsView.kt | 62 ------- .../LoginExceptionsDeleteButtonViewHolder.kt | 28 ---- .../LoginExceptionsListItemViewHolder.kt | 50 ------ .../ExceptionsAdapter.kt | 78 --------- .../ExceptionsInteractor.kt | 29 ---- .../ExceptionsView.kt | 76 --------- .../TrackingProtectionExceptionsFragment.kt | 107 ------------- .../viewholders/ExceptionsHeaderViewHolder.kt | 17 -- .../ExceptionsListItemViewHolder.kt | 51 ------ .../main/res/layout/component_exceptions.xml | 4 +- .../delete_logins_exceptions_button.xml | 3 +- .../res/layout/exceptions_description.xml | 3 +- app/src/main/res/navigation/nav_graph.xml | 4 +- .../login/LoginExceptionFragmentStoreTest.kt | 26 +++ .../login/LoginExceptionsAdapterTest.kt | 149 +++++++++++++++++ .../login/LoginExceptionsInteractorTest.kt | 42 +++++ .../login}/LoginExceptionsViewTest.kt | 15 +- ...TrackingProtectionExceptionsAdapterTest.kt | 151 ++++++++++++++++++ ...gProtectionExceptionsFragmentStoreTest.kt} | 15 +- ...ckingProtectionExceptionsInteractorTest.kt | 88 ++++++++++ .../TrackingProtectionExceptionsViewTest.kt} | 27 ++-- .../ExceptionsDeleteButtonViewHolderTest.kt} | 23 ++- .../ExceptionsHeaderViewHolderTest.kt} | 6 +- .../ExceptionsListItemViewHolderTest.kt | 74 +++++++++ .../LoginExceptionsAdapterTest.kt | 113 ------------- .../LoginExceptionsInteractorTest.kt | 36 ----- .../LoginExceptionsListItemViewHolderTest.kt | 63 -------- .../ExceptionsAdapterTest.kt | 42 ----- .../ExceptionsInteractorTest.kt | 54 ------- ...ngProtectionExceptionsFragmentStoreTest.kt | 35 ---- .../ExceptionsDeleteButtonViewHolderTest.kt | 43 ----- .../ExceptionsListItemViewHolderTest.kt | 56 ------- 51 files changed, 1116 insertions(+), 1144 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsAdapter.kt create mode 100644 app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsInteractor.kt create mode 100644 app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsView.kt rename app/src/main/java/org/mozilla/fenix/{loginexceptions => exceptions/login}/ExceptionsFragmentStore.kt (95%) create mode 100644 app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsAdapter.kt rename app/src/main/java/org/mozilla/fenix/{loginexceptions => exceptions/login}/LoginExceptionsFragment.kt (70%) create mode 100644 app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsInteractor.kt create mode 100644 app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsView.kt rename app/src/main/java/org/mozilla/fenix/{trackingprotectionexceptions => exceptions/trackingprotection}/ExceptionsFragmentStore.kt (85%) create mode 100644 app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsAdapter.kt create mode 100644 app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsFragment.kt create mode 100644 app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsInteractor.kt create mode 100644 app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsView.kt rename app/src/main/java/org/mozilla/fenix/{trackingprotectionexceptions => exceptions}/viewholders/ExceptionsDeleteButtonViewHolder.kt (55%) rename app/src/main/java/org/mozilla/fenix/{loginexceptions/viewholders/LoginExceptionsHeaderViewHolder.kt => exceptions/viewholders/ExceptionsHeaderViewHolder.kt} (68%) create mode 100644 app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsListItemViewHolder.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsAdapter.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsInteractor.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsView.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsDeleteButtonViewHolder.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsListItemViewHolder.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsAdapter.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsInteractor.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsView.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/TrackingProtectionExceptionsFragment.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsHeaderViewHolder.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsListItemViewHolder.kt create mode 100644 app/src/test/java/org/mozilla/fenix/exceptions/login/LoginExceptionFragmentStoreTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/exceptions/login/LoginExceptionsAdapterTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/exceptions/login/LoginExceptionsInteractorTest.kt rename app/src/test/java/org/mozilla/fenix/{loginexceptions => exceptions/login}/LoginExceptionsViewTest.kt (86%) create mode 100644 app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsAdapterTest.kt rename app/src/test/java/org/mozilla/fenix/{loginexceptions/LoginExceptionFragmentStoreTest.kt => exceptions/trackingprotection/TrackingProtectionExceptionsFragmentStoreTest.kt} (63%) create mode 100644 app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsInteractorTest.kt rename app/src/test/java/org/mozilla/fenix/{trackingprotectionexceptions/ExceptionsViewTest.kt => exceptions/trackingprotection/TrackingProtectionExceptionsViewTest.kt} (71%) rename app/src/test/java/org/mozilla/fenix/{loginexceptions/viewholders/LoginExceptionsDeleteButtonViewHolderTest.kt => exceptions/viewholders/ExceptionsDeleteButtonViewHolderTest.kt} (59%) rename app/src/test/java/org/mozilla/fenix/{loginexceptions/viewholders/LoginExceptionsHeaderViewHolderTest.kt => exceptions/viewholders/ExceptionsHeaderViewHolderTest.kt} (84%) create mode 100644 app/src/test/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsListItemViewHolderTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionsAdapterTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionsInteractorTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsListItemViewHolderTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsAdapterTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsInteractorTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/TrackingProtectionExceptionsFragmentStoreTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsDeleteButtonViewHolderTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsListItemViewHolderTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 5cf4e4e3c..a03672372 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -67,6 +67,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager 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.components import org.mozilla.fenix.ext.metrics @@ -98,7 +99,6 @@ import org.mozilla.fenix.sync.SyncedTabsFragmentDirections import org.mozilla.fenix.tabtray.TabTrayDialogFragment import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.ThemeManager -import org.mozilla.fenix.trackingprotectionexceptions.TrackingProtectionExceptionsFragmentDirections import org.mozilla.fenix.utils.BrowsersCache /** diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsAdapter.kt b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsAdapter.kt new file mode 100644 index 000000000..b0050ecf8 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsAdapter.kt @@ -0,0 +1,100 @@ +/* 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.exceptions + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.annotation.StringRes +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.fenix.exceptions.viewholders.ExceptionsDeleteButtonViewHolder +import org.mozilla.fenix.exceptions.viewholders.ExceptionsHeaderViewHolder +import org.mozilla.fenix.exceptions.viewholders.ExceptionsListItemViewHolder + +/** + * Adapter for a list of sites that are exempted from saving logins or tracking protection, + * along with controls to remove the exception. + */ +abstract class ExceptionsAdapter( + private val interactor: ExceptionsInteractor, + diffCallback: DiffUtil.ItemCallback +) : ListAdapter(diffCallback) { + + /** + * Change the list of items that are displayed. + * Header and footer items are added to the list as well. + */ + fun updateData(exceptions: List) { + val adapterItems: List = listOf(AdapterItem.Header) + + exceptions.map { wrapAdapterItem(it) } + + listOf(AdapterItem.DeleteButton) + submitList(adapterItems) + } + + /** + * Layout to use for the delete button. + */ + @get:LayoutRes + abstract val deleteButtonLayoutId: Int + + /** + * String to use for the exceptions list header. + */ + @get:StringRes + abstract val headerDescriptionResource: Int + + /** + * Converts an item from [updateData] into an adapter item. + */ + abstract fun wrapAdapterItem(item: T): AdapterItem.Item + + final override fun getItemViewType(position: Int) = when (getItem(position)) { + AdapterItem.DeleteButton -> deleteButtonLayoutId + AdapterItem.Header -> ExceptionsHeaderViewHolder.LAYOUT_ID + is AdapterItem.Item<*> -> ExceptionsListItemViewHolder.LAYOUT_ID + } + + final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) + + return when (viewType) { + deleteButtonLayoutId -> + ExceptionsDeleteButtonViewHolder(view, interactor) + ExceptionsHeaderViewHolder.LAYOUT_ID -> + ExceptionsHeaderViewHolder(view, headerDescriptionResource) + ExceptionsListItemViewHolder.LAYOUT_ID -> + ExceptionsListItemViewHolder(view, interactor) + else -> throw IllegalStateException() + } + } + + @Suppress("Unchecked_Cast") + final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is ExceptionsListItemViewHolder<*>) { + holder as ExceptionsListItemViewHolder + val adapterItem = getItem(position) as AdapterItem.Item + holder.bind(adapterItem.item, adapterItem.url) + } + } + + /** + * Internal items for [ExceptionsAdapter] + */ + sealed class AdapterItem { + object DeleteButton : AdapterItem() + object Header : AdapterItem() + + /** + * Represents an item to display in [ExceptionsAdapter]. + * [T] should refer to the same value as in the [ExceptionsAdapter] and [ExceptionsInteractor]. + */ + abstract class Item : AdapterItem() { + abstract val item: T + abstract val url: String + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsInteractor.kt b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsInteractor.kt new file mode 100644 index 000000000..a1638052d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsInteractor.kt @@ -0,0 +1,21 @@ +/* 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.exceptions + +/** + * Interface for exceptions view interactors. This interface is implemented by objects that want + * to respond to user interaction on the [ExceptionsView]. + */ +interface ExceptionsInteractor { + /** + * Called whenever all exception items are deleted + */ + fun onDeleteAll() + + /** + * Called whenever one exception item is deleted + */ + fun onDeleteOne(item: T) +} diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsView.kt b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsView.kt new file mode 100644 index 000000000..f42fe66ac --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsView.kt @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.exceptions + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.component_exceptions.* +import org.mozilla.fenix.R + +/** + * View that contains and configures the Exceptions List + */ +abstract class ExceptionsView( + container: ViewGroup, + protected val interactor: ExceptionsInteractor +) : LayoutContainer { + + override val containerView: FrameLayout = LayoutInflater.from(container.context) + .inflate(R.layout.component_exceptions, container, true) + .findViewById(R.id.exceptions_wrapper) + + protected abstract val exceptionsAdapter: ExceptionsAdapter + + init { + exceptions_list.apply { + layoutManager = LinearLayoutManager(containerView.context) + } + } + + fun update(items: List) { + exceptions_empty_view.isVisible = items.isEmpty() + exceptions_list.isVisible = items.isNotEmpty() + exceptionsAdapter.updateData(items) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/loginexceptions/ExceptionsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/exceptions/login/ExceptionsFragmentStore.kt similarity index 95% rename from app/src/main/java/org/mozilla/fenix/loginexceptions/ExceptionsFragmentStore.kt rename to app/src/main/java/org/mozilla/fenix/exceptions/login/ExceptionsFragmentStore.kt index 2493d3f68..f669f2b4a 100644 --- a/app/src/main/java/org/mozilla/fenix/loginexceptions/ExceptionsFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/exceptions/login/ExceptionsFragmentStore.kt @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.fenix.loginexceptions +package org.mozilla.fenix.exceptions.login import mozilla.components.feature.logins.exceptions.LoginException import mozilla.components.lib.state.Action @@ -26,7 +26,7 @@ sealed class ExceptionsFragmentAction : Action { * The state for the Exceptions Screen * @property items List of exceptions to display */ -data class ExceptionsFragmentState(val items: List) : State +data class ExceptionsFragmentState(val items: List = emptyList()) : State /** * The ExceptionsState Reducer. diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsAdapter.kt b/app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsAdapter.kt new file mode 100644 index 000000000..ca94186f1 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsAdapter.kt @@ -0,0 +1,44 @@ +/* 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.exceptions.login + +import androidx.recyclerview.widget.DiffUtil +import mozilla.components.feature.logins.exceptions.LoginException +import org.mozilla.fenix.R +import org.mozilla.fenix.exceptions.ExceptionsAdapter + +/** + * Adapter for a list of sites that are exempted from saving logins, + * along with controls to remove the exception. + */ +class LoginExceptionsAdapter( + interactor: LoginExceptionsInteractor +) : ExceptionsAdapter(interactor, DiffCallback) { + + override val deleteButtonLayoutId = R.layout.delete_logins_exceptions_button + override val headerDescriptionResource = R.string.preferences_passwords_exceptions_description + + override fun wrapAdapterItem(item: LoginException) = + LoginAdapterItem(item) + + data class LoginAdapterItem( + override val item: LoginException + ) : AdapterItem.Item() { + override val url get() = item.origin + } + + internal object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = + when (oldItem) { + AdapterItem.DeleteButton, AdapterItem.Header -> oldItem === newItem + is LoginAdapterItem -> newItem is LoginAdapterItem && oldItem.item.id == newItem.item.id + else -> false + } + + @Suppress("DiffUtilEquals") + override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = + oldItem == newItem + } +} diff --git a/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsFragment.kt b/app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsFragment.kt similarity index 70% rename from app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsFragment.kt rename to app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsFragment.kt index c1519bb00..c0f7b4dec 100644 --- a/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsFragment.kt @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.fenix.loginexceptions +package org.mozilla.fenix.exceptions.login import android.os.Bundle import android.view.LayoutInflater @@ -13,10 +13,9 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.lifecycleScope import androidx.lifecycle.observe import kotlinx.android.synthetic.main.fragment_exceptions.view.* -import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch -import mozilla.components.feature.logins.exceptions.LoginException +import kotlinx.coroutines.plus import mozilla.components.lib.state.ext.consumeFrom import org.mozilla.fenix.R import org.mozilla.fenix.components.StoreProvider @@ -45,14 +44,17 @@ class LoginExceptionsFragment : Fragment() { val view = inflater.inflate(R.layout.fragment_exceptions, container, false) exceptionsStore = StoreProvider.get(this) { ExceptionsFragmentStore( - ExceptionsFragmentState( - items = listOf() - ) + ExceptionsFragmentState(items = emptyList()) ) } - exceptionsInteractor = - LoginExceptionsInteractor(::deleteOneItem, ::deleteAllItems) - exceptionsView = LoginExceptionsView(view.exceptionsLayout, exceptionsInteractor) + exceptionsInteractor = DefaultLoginExceptionsInteractor( + ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO, + loginExceptionStorage = requireComponents.core.loginExceptionStorage + ) + exceptionsView = LoginExceptionsView( + view.exceptionsLayout, + exceptionsInteractor + ) subscribeToLoginExceptions() return view } @@ -67,19 +69,7 @@ class LoginExceptionsFragment : Fragment() { @ExperimentalCoroutinesApi override fun onViewCreated(view: View, savedInstanceState: Bundle?) { consumeFrom(exceptionsStore) { - exceptionsView.update(it) - } - } - - private fun deleteAllItems() { - viewLifecycleOwner.lifecycleScope.launch(IO) { - requireComponents.core.loginExceptionStorage.deleteAllLoginExceptions() - } - } - - private fun deleteOneItem(item: LoginException) { - viewLifecycleOwner.lifecycleScope.launch(IO) { - requireComponents.core.loginExceptionStorage.removeLoginException(item) + exceptionsView.update(it.items) } } } diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsInteractor.kt b/app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsInteractor.kt new file mode 100644 index 000000000..0360dd614 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsInteractor.kt @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.exceptions.login + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import mozilla.components.feature.logins.exceptions.LoginException +import mozilla.components.feature.logins.exceptions.LoginExceptionStorage +import org.mozilla.fenix.exceptions.ExceptionsInteractor + +interface LoginExceptionsInteractor : ExceptionsInteractor + +class DefaultLoginExceptionsInteractor( + private val ioScope: CoroutineScope, + private val loginExceptionStorage: LoginExceptionStorage +) : LoginExceptionsInteractor { + + override fun onDeleteAll() { + ioScope.launch { + loginExceptionStorage.deleteAllLoginExceptions() + } + } + + override fun onDeleteOne(item: LoginException) { + ioScope.launch { + loginExceptionStorage.removeLoginException(item) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsView.kt b/app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsView.kt new file mode 100644 index 000000000..7d83cd40b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/exceptions/login/LoginExceptionsView.kt @@ -0,0 +1,29 @@ +/* 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.exceptions.login + +import android.view.ViewGroup +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.component_exceptions.* +import mozilla.components.feature.logins.exceptions.LoginException +import org.mozilla.fenix.R +import org.mozilla.fenix.exceptions.ExceptionsView + +class LoginExceptionsView( + container: ViewGroup, + interactor: LoginExceptionsInteractor +) : ExceptionsView(container, interactor) { + + override val exceptionsAdapter = LoginExceptionsAdapter(interactor) + + init { + exceptions_learn_more.isVisible = false + exceptions_empty_message.text = + containerView.context.getString(R.string.preferences_passwords_exceptions_description_empty) + exceptions_list.apply { + adapter = exceptionsAdapter + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/ExceptionsFragmentStore.kt similarity index 85% rename from app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsFragmentStore.kt rename to app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/ExceptionsFragmentStore.kt index 1c2f5e982..6fa287e6d 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/ExceptionsFragmentStore.kt @@ -2,19 +2,13 @@ * 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.trackingprotectionexceptions +package org.mozilla.fenix.exceptions.trackingprotection import mozilla.components.concept.engine.content.blocking.TrackingProtectionException import mozilla.components.lib.state.Action import mozilla.components.lib.state.State import mozilla.components.lib.state.Store -/** - * Class representing an exception item - * @property url Host of the exception - */ -data class ExceptionItem(override val url: String) : TrackingProtectionException - /** * The [Store] for holding the [ExceptionsFragmentState] and applying [ExceptionsFragmentAction]s. */ @@ -32,7 +26,7 @@ sealed class ExceptionsFragmentAction : Action { * The state for the Exceptions Screen * @property items List of exceptions to display */ -data class ExceptionsFragmentState(val items: List) : State +data class ExceptionsFragmentState(val items: List = emptyList()) : State /** * The ExceptionsState Reducer. diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsAdapter.kt b/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsAdapter.kt new file mode 100644 index 000000000..c69fdd620 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsAdapter.kt @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.exceptions.trackingprotection + +import androidx.recyclerview.widget.DiffUtil +import mozilla.components.concept.engine.content.blocking.TrackingProtectionException +import org.mozilla.fenix.R +import org.mozilla.fenix.exceptions.ExceptionsAdapter + +/** + * Adapter for a list of sites that are exempted from Tracking Protection, + * along with controls to remove the exception. + */ +class TrackingProtectionExceptionsAdapter( + interactor: TrackingProtectionExceptionsInteractor +) : ExceptionsAdapter(interactor, DiffCallback) { + + override val deleteButtonLayoutId = R.layout.delete_exceptions_button + override val headerDescriptionResource = R.string.enhanced_tracking_protection_exceptions + + override fun wrapAdapterItem(item: TrackingProtectionException) = + TrackingProtectionAdapterItem(item) + + data class TrackingProtectionAdapterItem( + override val item: TrackingProtectionException + ) : AdapterItem.Item() { + override val url get() = item.url + } + + internal object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = + when (oldItem) { + AdapterItem.DeleteButton, AdapterItem.Header -> oldItem === newItem + is TrackingProtectionAdapterItem -> + newItem is TrackingProtectionAdapterItem && oldItem.item.url == newItem.item.url + else -> false + } + + @Suppress("DiffUtilEquals") + override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = + oldItem == newItem + } +} diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsFragment.kt b/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsFragment.kt new file mode 100644 index 000000000..eef5c4eb5 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsFragment.kt @@ -0,0 +1,66 @@ +/* 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.exceptions.trackingprotection + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import kotlinx.android.synthetic.main.fragment_exceptions.view.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.components.lib.state.ext.consumeFrom +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.components.StoreProvider +import org.mozilla.fenix.ext.requireComponents +import org.mozilla.fenix.ext.showToolbar + +/** + * Displays a list of sites that are exempted from Tracking Protection, + * along with controls to remove the exception. + */ +class TrackingProtectionExceptionsFragment : Fragment() { + + private lateinit var exceptionsStore: ExceptionsFragmentStore + private lateinit var exceptionsView: TrackingProtectionExceptionsView + private lateinit var exceptionsInteractor: DefaultTrackingProtectionExceptionsInteractor + + override fun onResume() { + super.onResume() + showToolbar(getString(R.string.preference_exceptions)) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_exceptions, container, false) + exceptionsStore = StoreProvider.get(this) { + ExceptionsFragmentStore( + ExceptionsFragmentState(items = emptyList()) + ) + } + exceptionsInteractor = DefaultTrackingProtectionExceptionsInteractor( + activity = activity as HomeActivity, + exceptionsStore = exceptionsStore, + trackingProtectionUseCases = requireComponents.useCases.trackingProtectionUseCases + ) + exceptionsView = TrackingProtectionExceptionsView( + view.exceptionsLayout, + exceptionsInteractor + ) + exceptionsInteractor.reloadExceptions() + return view + } + + @ExperimentalCoroutinesApi + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + consumeFrom(exceptionsStore) { + exceptionsView.update(it.items) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsInteractor.kt b/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsInteractor.kt new file mode 100644 index 000000000..f02a32b7e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsInteractor.kt @@ -0,0 +1,54 @@ +/* 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.exceptions.trackingprotection + +import mozilla.components.concept.engine.content.blocking.TrackingProtectionException +import mozilla.components.feature.session.TrackingProtectionUseCases +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.exceptions.ExceptionsInteractor +import org.mozilla.fenix.settings.SupportUtils + +interface TrackingProtectionExceptionsInteractor : ExceptionsInteractor { + /** + * Called whenever learn more about tracking protection is tapped + */ + fun onLearnMore() +} + +class DefaultTrackingProtectionExceptionsInteractor( + private val activity: HomeActivity, + private val exceptionsStore: ExceptionsFragmentStore, + private val trackingProtectionUseCases: TrackingProtectionUseCases +) : TrackingProtectionExceptionsInteractor { + + override fun onLearnMore() { + activity.openToBrowserAndLoad( + searchTermOrURL = SupportUtils.getGenericSumoURLForTopic( + SupportUtils.SumoTopic.TRACKING_PROTECTION + ), + newTab = true, + from = BrowserDirection.FromTrackingProtectionExceptions + ) + } + + override fun onDeleteAll() { + trackingProtectionUseCases.removeAllExceptions() + reloadExceptions() + } + + override fun onDeleteOne(item: TrackingProtectionException) { + trackingProtectionUseCases.removeException(item) + reloadExceptions() + } + + fun reloadExceptions() { + trackingProtectionUseCases.fetchExceptions { resultList -> + exceptionsStore.dispatch( + ExceptionsFragmentAction.Change(resultList) + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsView.kt b/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsView.kt new file mode 100644 index 000000000..b312e939e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsView.kt @@ -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.exceptions.trackingprotection + +import android.text.method.LinkMovementMethod +import android.view.ViewGroup +import kotlinx.android.synthetic.main.component_exceptions.* +import mozilla.components.concept.engine.content.blocking.TrackingProtectionException +import org.mozilla.fenix.exceptions.ExceptionsView +import org.mozilla.fenix.ext.addUnderline + +class TrackingProtectionExceptionsView( + container: ViewGroup, + interactor: TrackingProtectionExceptionsInteractor +) : ExceptionsView(container, interactor) { + + override val exceptionsAdapter = TrackingProtectionExceptionsAdapter(interactor) + + init { + exceptions_list.apply { + adapter = exceptionsAdapter + } + + with(exceptions_learn_more) { + addUnderline() + + movementMethod = LinkMovementMethod.getInstance() + setOnClickListener { interactor.onLearnMore() } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsDeleteButtonViewHolder.kt b/app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsDeleteButtonViewHolder.kt similarity index 55% rename from app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsDeleteButtonViewHolder.kt rename to app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsDeleteButtonViewHolder.kt index d25754bdf..2c8af4f1f 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsDeleteButtonViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsDeleteButtonViewHolder.kt @@ -2,27 +2,23 @@ * 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.trackingprotectionexceptions.viewholders +package org.mozilla.fenix.exceptions.viewholders import android.view.View import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.delete_exceptions_button.view.* +import com.google.android.material.button.MaterialButton import org.mozilla.fenix.R -import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsInteractor +import org.mozilla.fenix.exceptions.ExceptionsInteractor class ExceptionsDeleteButtonViewHolder( view: View, - private val interactor: ExceptionsInteractor + private val interactor: ExceptionsInteractor<*> ) : RecyclerView.ViewHolder(view) { - private val deleteButton = view.removeAllExceptions init { + val deleteButton: MaterialButton = view.findViewById(R.id.removeAllExceptions) deleteButton.setOnClickListener { interactor.onDeleteAll() } } - - companion object { - const val LAYOUT_ID = R.layout.delete_exceptions_button - } } diff --git a/app/src/main/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsHeaderViewHolder.kt b/app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsHeaderViewHolder.kt similarity index 68% rename from app/src/main/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsHeaderViewHolder.kt rename to app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsHeaderViewHolder.kt index 7733563f7..97374247c 100644 --- a/app/src/main/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsHeaderViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsHeaderViewHolder.kt @@ -2,20 +2,21 @@ * 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.loginexceptions.viewholders +package org.mozilla.fenix.exceptions.viewholders import android.view.View +import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.exceptions_description.view.* import org.mozilla.fenix.R -class LoginExceptionsHeaderViewHolder( - view: View +class ExceptionsHeaderViewHolder( + view: View, + @StringRes description: Int ) : RecyclerView.ViewHolder(view) { init { - view.exceptions_description.text = - view.context.getString(R.string.preferences_passwords_exceptions_description) + view.exceptions_description.text = view.context.getString(description) } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsListItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsListItemViewHolder.kt new file mode 100644 index 000000000..d3225d34f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsListItemViewHolder.kt @@ -0,0 +1,42 @@ +/* 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.exceptions.viewholders + +import android.view.View +import kotlinx.android.synthetic.main.exception_item.* +import mozilla.components.browser.icons.BrowserIcons +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( + view: View, + private val interactor: ExceptionsInteractor, + private val icons: BrowserIcons = view.context.components.core.icons +) : ViewHolder(view) { + + private lateinit var item: T + + init { + delete_exception.setOnClickListener { + interactor.onDeleteOne(item) + } + } + + fun bind(item: T, url: String) { + this.item = item + webAddressView.text = url + icons.loadIntoView(favicon_image, url) + } + + companion object { + const val LAYOUT_ID = R.layout.exception_item + } +} diff --git a/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsAdapter.kt b/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsAdapter.kt deleted file mode 100644 index a04a705fe..000000000 --- a/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsAdapter.kt +++ /dev/null @@ -1,83 +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.loginexceptions - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import mozilla.components.feature.logins.exceptions.LoginException -import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsDeleteButtonViewHolder -import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsHeaderViewHolder -import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsListItemViewHolder - -/** - * Adapter for a list of sites that are exempted from saving logins, - * along with controls to remove the exception. - */ -class LoginExceptionsAdapter( - private val interactor: LoginExceptionsInteractor -) : ListAdapter(DiffCallback) { - - /** - * Change the list of items that are displayed. - * Header and footer items are added to the list as well. - */ - fun updateData(exceptions: List) { - val adapterItems: List = listOf(AdapterItem.Header) + - exceptions.map { AdapterItem.Item(it) } + - listOf(AdapterItem.DeleteButton) - submitList(adapterItems) - } - - override fun getItemViewType(position: Int) = when (getItem(position)) { - AdapterItem.DeleteButton -> LoginExceptionsDeleteButtonViewHolder.LAYOUT_ID - AdapterItem.Header -> LoginExceptionsHeaderViewHolder.LAYOUT_ID - is AdapterItem.Item -> LoginExceptionsListItemViewHolder.LAYOUT_ID - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) - - return when (viewType) { - LoginExceptionsDeleteButtonViewHolder.LAYOUT_ID -> LoginExceptionsDeleteButtonViewHolder( - view, - interactor - ) - LoginExceptionsHeaderViewHolder.LAYOUT_ID -> LoginExceptionsHeaderViewHolder(view) - LoginExceptionsListItemViewHolder.LAYOUT_ID -> LoginExceptionsListItemViewHolder( - view, - interactor - ) - else -> throw IllegalStateException() - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - if (holder is LoginExceptionsListItemViewHolder) { - val adapterItem = getItem(position) as AdapterItem.Item - holder.bind(adapterItem.item) - } - } - - sealed class AdapterItem { - object DeleteButton : AdapterItem() - object Header : AdapterItem() - data class Item(val item: LoginException) : AdapterItem() - } - - internal object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = - when (oldItem) { - AdapterItem.DeleteButton, AdapterItem.Header -> oldItem === newItem - is AdapterItem.Item -> newItem is AdapterItem.Item && oldItem.item.id == newItem.item.id - } - - @Suppress("DiffUtilEquals") - override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = - oldItem == newItem - } -} diff --git a/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsInteractor.kt b/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsInteractor.kt deleted file mode 100644 index 688ca4437..000000000 --- a/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsInteractor.kt +++ /dev/null @@ -1,24 +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.loginexceptions - -import mozilla.components.feature.logins.exceptions.LoginException - -/** - * Interactor for the exceptions screen - * Provides implementations for the ExceptionsViewInteractor - */ -class LoginExceptionsInteractor( - private val deleteOne: (LoginException) -> Unit, - private val deleteAll: () -> Unit -) : ExceptionsViewInteractor { - override fun onDeleteAll() { - deleteAll.invoke() - } - - override fun onDeleteOne(item: LoginException) { - deleteOne.invoke(item) - } -} diff --git a/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsView.kt b/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsView.kt deleted file mode 100644 index f6924870b..000000000 --- a/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsView.kt +++ /dev/null @@ -1,62 +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.loginexceptions - -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.core.view.isVisible -import androidx.recyclerview.widget.LinearLayoutManager -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.component_exceptions.* -import mozilla.components.feature.logins.exceptions.LoginException -import org.mozilla.fenix.R - -/** - * Interface for the ExceptionsViewInteractor. This interface is implemented by objects that want - * to respond to user interaction on the ExceptionsView - */ -interface ExceptionsViewInteractor { - /** - * Called whenever all exception items are deleted - */ - fun onDeleteAll() - - /** - * Called whenever one exception item is deleted - */ - fun onDeleteOne(item: LoginException) -} - -/** - * View that contains and configures the Exceptions List - */ -class LoginExceptionsView( - container: ViewGroup, - val interactor: LoginExceptionsInteractor -) : LayoutContainer { - - override val containerView: FrameLayout = LayoutInflater.from(container.context) - .inflate(R.layout.component_exceptions, container, true) - .findViewById(R.id.exceptions_wrapper) - - private val exceptionsAdapter = LoginExceptionsAdapter(interactor) - - init { - exceptions_learn_more.isVisible = false - exceptions_empty_message.text = - containerView.context.getString(R.string.preferences_passwords_exceptions_description_empty) - exceptions_list.apply { - adapter = exceptionsAdapter - layoutManager = LinearLayoutManager(containerView.context) - } - } - - fun update(state: ExceptionsFragmentState) { - exceptions_empty_view.isVisible = state.items.isEmpty() - exceptions_list.isVisible = state.items.isNotEmpty() - exceptionsAdapter.updateData(state.items) - } -} diff --git a/app/src/main/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsDeleteButtonViewHolder.kt b/app/src/main/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsDeleteButtonViewHolder.kt deleted file mode 100644 index a69ecf312..000000000 --- a/app/src/main/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsDeleteButtonViewHolder.kt +++ /dev/null @@ -1,28 +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.loginexceptions.viewholders - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.delete_exceptions_button.view.* -import org.mozilla.fenix.R -import org.mozilla.fenix.loginexceptions.LoginExceptionsInteractor - -class LoginExceptionsDeleteButtonViewHolder( - view: View, - private val interactor: LoginExceptionsInteractor -) : RecyclerView.ViewHolder(view) { - private val deleteButton = view.removeAllExceptions - - init { - deleteButton.setOnClickListener { - interactor.onDeleteAll() - } - } - - companion object { - const val LAYOUT_ID = R.layout.delete_logins_exceptions_button - } -} diff --git a/app/src/main/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsListItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsListItemViewHolder.kt deleted file mode 100644 index 688da089a..000000000 --- a/app/src/main/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsListItemViewHolder.kt +++ /dev/null @@ -1,50 +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.loginexceptions.viewholders - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.exception_item.view.* -import mozilla.components.feature.logins.exceptions.LoginException -import org.mozilla.fenix.R -import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.loadIntoView -import org.mozilla.fenix.loginexceptions.LoginExceptionsInteractor - -/** - * View holder for a single website that is exempted from Tracking Protection. - */ -class LoginExceptionsListItemViewHolder( - view: View, - private val interactor: LoginExceptionsInteractor -) : RecyclerView.ViewHolder(view) { - - private val favicon = view.favicon_image - private val url = view.webAddressView - private val deleteButton = view.delete_exception - - private var item: LoginException? = null - - init { - deleteButton.setOnClickListener { - item?.let { - interactor.onDeleteOne(it) - } - } - } - - fun bind(item: LoginException) { - this.item = item - url.text = item.origin - } - - private fun updateFavIcon(url: String) { - favicon.context.components.core.icons.loadIntoView(favicon, url) - } - - companion object { - const val LAYOUT_ID = R.layout.exception_item - } -} diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsAdapter.kt b/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsAdapter.kt deleted file mode 100644 index a2adfdd5d..000000000 --- a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsAdapter.kt +++ /dev/null @@ -1,78 +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.trackingprotectionexceptions - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import mozilla.components.concept.engine.content.blocking.TrackingProtectionException -import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsDeleteButtonViewHolder -import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsHeaderViewHolder -import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsListItemViewHolder - -/** - * Adapter for a list of sites that are exempted from Tracking Protection, - * along with controls to remove the exception. - */ -class ExceptionsAdapter( - private val interactor: ExceptionsInteractor -) : ListAdapter(DiffCallback) { - - /** - * Change the list of items that are displayed. - * Header and footer items are added to the list as well. - */ - fun updateData(exceptions: List) { - val adapterItems = mutableListOf() - adapterItems.add(AdapterItem.Header) - exceptions.mapTo(adapterItems) { AdapterItem.Item(it) } - adapterItems.add(AdapterItem.DeleteButton) - submitList(adapterItems) - } - - override fun getItemViewType(position: Int) = when (getItem(position)) { - AdapterItem.DeleteButton -> ExceptionsDeleteButtonViewHolder.LAYOUT_ID - AdapterItem.Header -> ExceptionsHeaderViewHolder.LAYOUT_ID - is AdapterItem.Item -> ExceptionsListItemViewHolder.LAYOUT_ID - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) - - return when (viewType) { - ExceptionsDeleteButtonViewHolder.LAYOUT_ID -> ExceptionsDeleteButtonViewHolder( - view, - interactor - ) - ExceptionsHeaderViewHolder.LAYOUT_ID -> ExceptionsHeaderViewHolder(view) - ExceptionsListItemViewHolder.LAYOUT_ID -> ExceptionsListItemViewHolder(view, interactor) - else -> throw IllegalStateException() - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - if (holder is ExceptionsListItemViewHolder) { - val adapterItem = getItem(position) as AdapterItem.Item - holder.bind(adapterItem.item) - } - } - - sealed class AdapterItem { - object DeleteButton : AdapterItem() - object Header : AdapterItem() - data class Item(val item: TrackingProtectionException) : AdapterItem() - } - - private object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = - areContentsTheSame(oldItem, newItem) - - @Suppress("DiffUtilEquals") - override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = - oldItem == newItem - } -} diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsInteractor.kt b/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsInteractor.kt deleted file mode 100644 index 79694ac1c..000000000 --- a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsInteractor.kt +++ /dev/null @@ -1,29 +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.trackingprotectionexceptions - -import mozilla.components.concept.engine.content.blocking.TrackingProtectionException - -/** - * Interactor for the exceptions screen - * Provides implementations for the ExceptionsViewInteractor - */ -class ExceptionsInteractor( - private val learnMore: () -> Unit, - private val deleteOne: (TrackingProtectionException) -> Unit, - private val deleteAll: () -> Unit -) : ExceptionsViewInteractor { - override fun onLearnMore() { - learnMore.invoke() - } - - override fun onDeleteAll() { - deleteAll.invoke() - } - - override fun onDeleteOne(item: TrackingProtectionException) { - deleteOne.invoke(item) - } -} diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsView.kt b/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsView.kt deleted file mode 100644 index fcc58f7ce..000000000 --- a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsView.kt +++ /dev/null @@ -1,76 +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.trackingprotectionexceptions - -import android.text.method.LinkMovementMethod -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.core.view.isVisible -import androidx.recyclerview.widget.LinearLayoutManager -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.component_exceptions.* -import mozilla.components.concept.engine.content.blocking.TrackingProtectionException -import org.mozilla.fenix.R -import org.mozilla.fenix.ext.addUnderline - -/** - * Interface for the ExceptionsViewInteractor. This interface is implemented by objects that want - * to respond to user interaction on the ExceptionsView - */ -interface ExceptionsViewInteractor { - /** - * Called whenever learn more about tracking protection is tapped - */ - fun onLearnMore() - - /** - * Called whenever all exception items are deleted - */ - fun onDeleteAll() - - /** - * Called whenever one exception item is deleted - */ - fun onDeleteOne(item: TrackingProtectionException) -} - -/** - * View that contains and configures the Exceptions List - */ -class ExceptionsView( - container: ViewGroup, - interactor: ExceptionsInteractor -) : LayoutContainer { - - override val containerView: FrameLayout = LayoutInflater.from(container.context) - .inflate(R.layout.component_exceptions, container, true) - .findViewById(R.id.exceptions_wrapper) - - private val exceptionsAdapter = - ExceptionsAdapter( - interactor - ) - - init { - exceptions_list.apply { - adapter = exceptionsAdapter - layoutManager = LinearLayoutManager(container.context) - } - - with(exceptions_learn_more) { - addUnderline() - - movementMethod = LinkMovementMethod.getInstance() - setOnClickListener { interactor.onLearnMore() } - } - } - - fun update(state: ExceptionsFragmentState) { - exceptions_empty_view.isVisible = state.items.isEmpty() - exceptions_list.isVisible = state.items.isNotEmpty() - exceptionsAdapter.updateData(state.items) - } -} diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/TrackingProtectionExceptionsFragment.kt b/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/TrackingProtectionExceptionsFragment.kt deleted file mode 100644 index 79b37e13c..000000000 --- a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/TrackingProtectionExceptionsFragment.kt +++ /dev/null @@ -1,107 +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.trackingprotectionexceptions - -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import kotlinx.android.synthetic.main.fragment_exceptions.view.* -import kotlinx.coroutines.ExperimentalCoroutinesApi -import mozilla.components.concept.engine.content.blocking.TrackingProtectionException -import mozilla.components.feature.session.TrackingProtectionUseCases -import mozilla.components.lib.state.ext.consumeFrom -import org.mozilla.fenix.BrowserDirection -import org.mozilla.fenix.HomeActivity -import org.mozilla.fenix.R -import org.mozilla.fenix.components.StoreProvider -import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.showToolbar -import org.mozilla.fenix.settings.SupportUtils - -/** - * Displays a list of sites that are exempted from Tracking Protection, - * along with controls to remove the exception. - */ -class TrackingProtectionExceptionsFragment : Fragment() { - - private lateinit var exceptionsStore: ExceptionsFragmentStore - private lateinit var exceptionsView: ExceptionsView - private lateinit var exceptionsInteractor: ExceptionsInteractor - private lateinit var trackingProtectionUseCases: TrackingProtectionUseCases - - override fun onResume() { - super.onResume() - showToolbar(getString(R.string.preference_exceptions)) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val view = inflater.inflate(R.layout.fragment_exceptions, container, false) - trackingProtectionUseCases = view.context.components.useCases.trackingProtectionUseCases - exceptionsStore = StoreProvider.get(this) { - ExceptionsFragmentStore( - ExceptionsFragmentState( - items = emptyList() - ) - ) - } - exceptionsInteractor = - ExceptionsInteractor( - ::openLearnMore, - ::deleteOneItem, - ::deleteAllItems - ) - exceptionsView = - ExceptionsView( - view.exceptionsLayout, - exceptionsInteractor - ) - reloadExceptions() - return view - } - - @ExperimentalCoroutinesApi - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - consumeFrom(exceptionsStore) { - exceptionsView.update(it) - } - } - - private fun deleteAllItems() { - trackingProtectionUseCases.removeAllExceptions() - reloadExceptions() - } - - private fun deleteOneItem(item: TrackingProtectionException) { - trackingProtectionUseCases.removeException(item) - Log.e("Remove one exception", "$item") - reloadExceptions() - } - - private fun openLearnMore() { - (activity as HomeActivity).openToBrowserAndLoad( - searchTermOrURL = SupportUtils.getGenericSumoURLForTopic - (SupportUtils.SumoTopic.TRACKING_PROTECTION), - newTab = true, - from = BrowserDirection.FromTrackingProtectionExceptions - ) - } - - private fun reloadExceptions() { - trackingProtectionUseCases.fetchExceptions { resultList -> - exceptionsStore.dispatch( - ExceptionsFragmentAction.Change( - resultList - ) - ) - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsHeaderViewHolder.kt b/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsHeaderViewHolder.kt deleted file mode 100644 index 2a5c7b54d..000000000 --- a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsHeaderViewHolder.kt +++ /dev/null @@ -1,17 +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.trackingprotectionexceptions.viewholders - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import org.mozilla.fenix.R - -class ExceptionsHeaderViewHolder( - view: View -) : RecyclerView.ViewHolder(view) { - companion object { - const val LAYOUT_ID = R.layout.exceptions_description - } -} diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsListItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsListItemViewHolder.kt deleted file mode 100644 index ec43919b9..000000000 --- a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsListItemViewHolder.kt +++ /dev/null @@ -1,51 +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.trackingprotectionexceptions.viewholders - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.exception_item.view.* -import mozilla.components.concept.engine.content.blocking.TrackingProtectionException -import org.mozilla.fenix.R -import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsInteractor -import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.loadIntoView - -/** - * View holder for a single website that is exempted from Tracking Protection. - */ -class ExceptionsListItemViewHolder( - view: View, - private val interactor: ExceptionsInteractor -) : RecyclerView.ViewHolder(view) { - - private val favicon = view.favicon_image - private val url = view.webAddressView - private val deleteButton = view.delete_exception - - private var item: TrackingProtectionException? = null - - init { - deleteButton.setOnClickListener { - item?.let { - interactor.onDeleteOne(it) - } - } - } - - fun bind(item: TrackingProtectionException) { - this.item = item - url.text = item.url - updateFavIcon(item.url) - } - - private fun updateFavIcon(url: String) { - favicon.context.components.core.icons.loadIntoView(favicon, url) - } - - companion object { - const val LAYOUT_ID = R.layout.exception_item - } -} diff --git a/app/src/main/res/layout/component_exceptions.xml b/app/src/main/res/layout/component_exceptions.xml index 00d64c282..8dd612334 100644 --- a/app/src/main/res/layout/component_exceptions.xml +++ b/app/src/main/res/layout/component_exceptions.xml @@ -45,5 +45,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone" - tools:listitem="@layout/exception_item" /> + tools:listheader="@layout/exceptions_description" + tools:listitem="@layout/exception_item" + tools:listfooter="@layout/delete_exceptions_button" /> diff --git a/app/src/main/res/layout/delete_logins_exceptions_button.xml b/app/src/main/res/layout/delete_logins_exceptions_button.xml index 291e5e775..6b172eae9 100644 --- a/app/src/main/res/layout/delete_logins_exceptions_button.xml +++ b/app/src/main/res/layout/delete_logins_exceptions_button.xml @@ -2,7 +2,8 @@ - diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 3bd6c282d..55249ba73 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -347,7 +347,7 @@ @@ -609,7 +609,7 @@ android:label="@string/preferences_delete_browsing_data" /> adapter.getItemViewType(i) } + .map { viewType -> adapter.onCreateViewHolder(parent, viewType) } + .toList() + assertEquals(4, holders.size) + + assertTrue(holders[0] is ExceptionsHeaderViewHolder) + assertTrue(holders[1] is ExceptionsListItemViewHolder<*>) + assertTrue(holders[2] is ExceptionsListItemViewHolder<*>) + assertTrue(holders[3] is ExceptionsDeleteButtonViewHolder) + } + + @Test + fun `headers and delete should check if the other object is the same`() { + assertTrue( + LoginExceptionsAdapter.DiffCallback.areItemsTheSame( + ExceptionsAdapter.AdapterItem.Header, + ExceptionsAdapter.AdapterItem.Header + ) + ) + assertTrue( + LoginExceptionsAdapter.DiffCallback.areItemsTheSame( + ExceptionsAdapter.AdapterItem.DeleteButton, + ExceptionsAdapter.AdapterItem.DeleteButton + ) + ) + assertFalse( + LoginExceptionsAdapter.DiffCallback.areItemsTheSame( + ExceptionsAdapter.AdapterItem.Header, + ExceptionsAdapter.AdapterItem.DeleteButton + ) + ) + assertTrue( + LoginExceptionsAdapter.DiffCallback.areContentsTheSame( + ExceptionsAdapter.AdapterItem.Header, + ExceptionsAdapter.AdapterItem.Header + ) + ) + assertTrue( + LoginExceptionsAdapter.DiffCallback.areContentsTheSame( + ExceptionsAdapter.AdapterItem.DeleteButton, + ExceptionsAdapter.AdapterItem.DeleteButton + ) + ) + assertFalse( + LoginExceptionsAdapter.DiffCallback.areContentsTheSame( + ExceptionsAdapter.AdapterItem.DeleteButton, + ExceptionsAdapter.AdapterItem.Header + ) + ) + } + + @Test + fun `items with the same id should be marked as same`() { + assertTrue( + LoginExceptionsAdapter.DiffCallback.areItemsTheSame( + LoginExceptionsAdapter.LoginAdapterItem( + mockk { + every { id } returns 12L + } + ), + LoginExceptionsAdapter.LoginAdapterItem( + mockk { + every { id } returns 12L + } + ) + ) + ) + assertFalse( + LoginExceptionsAdapter.DiffCallback.areItemsTheSame( + LoginExceptionsAdapter.LoginAdapterItem( + mockk { + every { id } returns 14L + } + ), + LoginExceptionsAdapter.LoginAdapterItem( + mockk { + every { id } returns 12L + } + ) + ) + ) + assertFalse( + LoginExceptionsAdapter.DiffCallback.areItemsTheSame( + LoginExceptionsAdapter.LoginAdapterItem( + mockk { + every { id } returns 14L + } + ), + ExceptionsAdapter.AdapterItem.Header + ) + ) + assertFalse( + LoginExceptionsAdapter.DiffCallback.areItemsTheSame( + ExceptionsAdapter.AdapterItem.DeleteButton, + LoginExceptionsAdapter.LoginAdapterItem( + mockk { + every { id } returns 14L + } + ) + ) + ) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/exceptions/login/LoginExceptionsInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/exceptions/login/LoginExceptionsInteractorTest.kt new file mode 100644 index 000000000..b36f147e9 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/exceptions/login/LoginExceptionsInteractorTest.kt @@ -0,0 +1,42 @@ +/* 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.exceptions.login + +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.runBlockingTest +import mozilla.components.feature.logins.exceptions.LoginException +import mozilla.components.feature.logins.exceptions.LoginExceptionStorage +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class LoginExceptionsInteractorTest { + + private lateinit var loginExceptionStorage: LoginExceptionStorage + private lateinit var interactor: LoginExceptionsInteractor + private val scope = TestCoroutineScope() + + @Before + fun setup() { + loginExceptionStorage = mockk(relaxed = true) + interactor = DefaultLoginExceptionsInteractor(scope, loginExceptionStorage) + } + + @Test + fun onDeleteAll() = scope.runBlockingTest { + interactor.onDeleteAll() + verify { loginExceptionStorage.deleteAllLoginExceptions() } + } + + @Test + fun onDeleteOne() = scope.runBlockingTest { + val exceptionsItem: LoginException = mockk() + interactor.onDeleteOne(exceptionsItem) + verify { loginExceptionStorage.removeLoginException(exceptionsItem) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionsViewTest.kt b/app/src/test/java/org/mozilla/fenix/exceptions/login/LoginExceptionsViewTest.kt similarity index 86% rename from app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionsViewTest.kt rename to app/src/test/java/org/mozilla/fenix/exceptions/login/LoginExceptionsViewTest.kt index e716bffba..b3faeb0c5 100644 --- a/app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionsViewTest.kt +++ b/app/src/test/java/org/mozilla/fenix/exceptions/login/LoginExceptionsViewTest.kt @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.fenix.loginexceptions +package org.mozilla.fenix.exceptions.login import android.view.ViewGroup import android.widget.FrameLayout @@ -30,7 +30,10 @@ class LoginExceptionsViewTest { fun setup() { parent = FrameLayout(testContext) interactor = mockk() - view = LoginExceptionsView(parent, interactor) + view = LoginExceptionsView( + parent, + interactor + ) } @Test @@ -45,9 +48,7 @@ class LoginExceptionsViewTest { @Test fun `hide list when there are no items`() { - view.update(ExceptionsFragmentState( - items = emptyList() - )) + view.update(emptyList()) assertTrue(view.exceptions_empty_view.isVisible) assertFalse(view.exceptions_list.isVisible) @@ -55,9 +56,7 @@ class LoginExceptionsViewTest { @Test fun `shows list when there are items`() { - view.update(ExceptionsFragmentState( - items = listOf(mockk()) - )) + view.update(listOf(mockk())) assertFalse(view.exceptions_empty_view.isVisible) assertTrue(view.exceptions_list.isVisible) diff --git a/app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsAdapterTest.kt new file mode 100644 index 000000000..c4cd97501 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsAdapterTest.kt @@ -0,0 +1,151 @@ +/* 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.exceptions.trackingprotection + +import android.content.Context +import android.widget.FrameLayout +import androidx.appcompat.view.ContextThemeWrapper +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.R +import org.mozilla.fenix.exceptions.ExceptionsAdapter +import org.mozilla.fenix.exceptions.viewholders.ExceptionsDeleteButtonViewHolder +import org.mozilla.fenix.exceptions.viewholders.ExceptionsHeaderViewHolder +import org.mozilla.fenix.exceptions.viewholders.ExceptionsListItemViewHolder +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@ExperimentalCoroutinesApi +@RunWith(FenixRobolectricTestRunner::class) +class TrackingProtectionExceptionsAdapterTest { + + private lateinit var interactor: TrackingProtectionExceptionsInteractor + private lateinit var adapter: TrackingProtectionExceptionsAdapter + private lateinit var context: Context + + @Before + fun setup() { + interactor = mockk() + adapter = TrackingProtectionExceptionsAdapter(interactor) + context = ContextThemeWrapper(testContext, R.style.NormalTheme) + } + + @Test + fun `creates correct view holder type`() { + val parent = FrameLayout(context) + adapter.updateData(listOf(mockk(), mockk())) + assertEquals(4, adapter.itemCount) + + val holders = (0 until adapter.itemCount).asSequence() + .map { i -> adapter.getItemViewType(i) } + .map { viewType -> adapter.onCreateViewHolder(parent, viewType) } + .toList() + assertEquals(4, holders.size) + + assertTrue(holders[0] is ExceptionsHeaderViewHolder) + assertTrue(holders[1] is ExceptionsListItemViewHolder<*>) + assertTrue(holders[2] is ExceptionsListItemViewHolder<*>) + assertTrue(holders[3] is ExceptionsDeleteButtonViewHolder) + } + + @Test + fun `headers and delete should check if the other object is the same`() { + assertTrue( + TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame( + ExceptionsAdapter.AdapterItem.Header, + ExceptionsAdapter.AdapterItem.Header + ) + ) + assertTrue( + TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame( + ExceptionsAdapter.AdapterItem.DeleteButton, + ExceptionsAdapter.AdapterItem.DeleteButton + ) + ) + assertFalse( + TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame( + ExceptionsAdapter.AdapterItem.Header, + ExceptionsAdapter.AdapterItem.DeleteButton + ) + ) + assertTrue( + TrackingProtectionExceptionsAdapter.DiffCallback.areContentsTheSame( + ExceptionsAdapter.AdapterItem.Header, + ExceptionsAdapter.AdapterItem.Header + ) + ) + assertTrue( + TrackingProtectionExceptionsAdapter.DiffCallback.areContentsTheSame( + ExceptionsAdapter.AdapterItem.DeleteButton, + ExceptionsAdapter.AdapterItem.DeleteButton + ) + ) + assertFalse( + TrackingProtectionExceptionsAdapter.DiffCallback.areContentsTheSame( + ExceptionsAdapter.AdapterItem.DeleteButton, + ExceptionsAdapter.AdapterItem.Header + ) + ) + } + + @Test + fun `items with the same url should be marked as same`() { + assertTrue( + TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame( + TrackingProtectionExceptionsAdapter.TrackingProtectionAdapterItem( + mockk { + every { url } returns "https://mozilla.org" + } + ), + TrackingProtectionExceptionsAdapter.TrackingProtectionAdapterItem( + mockk { + every { url } returns "https://mozilla.org" + } + ) + ) + ) + assertFalse( + TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame( + TrackingProtectionExceptionsAdapter.TrackingProtectionAdapterItem( + mockk { + every { url } returns "https://mozilla.org" + } + ), + TrackingProtectionExceptionsAdapter.TrackingProtectionAdapterItem( + mockk { + every { url } returns "https://firefox.com" + } + ) + ) + ) + assertFalse( + TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame( + TrackingProtectionExceptionsAdapter.TrackingProtectionAdapterItem( + mockk { + every { url } returns "https://mozilla.org" + } + ), + ExceptionsAdapter.AdapterItem.Header + ) + ) + assertFalse( + TrackingProtectionExceptionsAdapter.DiffCallback.areItemsTheSame( + ExceptionsAdapter.AdapterItem.DeleteButton, + TrackingProtectionExceptionsAdapter.TrackingProtectionAdapterItem( + mockk { + every { url } returns "https://mozilla.org" + } + ) + ) + ) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionFragmentStoreTest.kt b/app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsFragmentStoreTest.kt similarity index 63% rename from app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionFragmentStoreTest.kt rename to app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsFragmentStoreTest.kt index ee0bd3171..06e6748df 100644 --- a/app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionFragmentStoreTest.kt +++ b/app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsFragmentStoreTest.kt @@ -2,21 +2,20 @@ 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.loginexceptions +package org.mozilla.fenix.exceptions.trackingprotection -import io.mockk.mockk import kotlinx.coroutines.runBlocking -import mozilla.components.feature.logins.exceptions.LoginException +import mozilla.components.concept.engine.content.blocking.TrackingProtectionException import org.junit.Assert.assertEquals import org.junit.Assert.assertNotSame import org.junit.Test -class LoginExceptionFragmentStoreTest { +class TrackingProtectionExceptionsFragmentStoreTest { @Test fun onChange() = runBlocking { - val initialState = emptyDefaultState() + val initialState = ExceptionsFragmentState() val store = ExceptionsFragmentStore(initialState) - val newExceptionsItem: LoginException = mockk() + val newExceptionsItem = ExceptionItem("URL") store.dispatch(ExceptionsFragmentAction.Change(listOf(newExceptionsItem))).join() assertNotSame(initialState, store.state) @@ -26,7 +25,5 @@ class LoginExceptionFragmentStoreTest { ) } - private fun emptyDefaultState(): ExceptionsFragmentState = ExceptionsFragmentState( - items = listOf() - ) + private data class ExceptionItem(override val url: String) : TrackingProtectionException } diff --git a/app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsInteractorTest.kt new file mode 100644 index 000000000..c841b384b --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsInteractorTest.kt @@ -0,0 +1,88 @@ +/* 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.exceptions.trackingprotection + +import io.mockk.CapturingSlot +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import io.mockk.verifySequence +import mozilla.components.concept.engine.content.blocking.TrackingProtectionException +import mozilla.components.feature.session.TrackingProtectionUseCases +import org.junit.Before +import org.junit.Test +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.settings.SupportUtils + +class TrackingProtectionExceptionsInteractorTest { + + @MockK(relaxed = true) private lateinit var activity: HomeActivity + @MockK(relaxed = true) private lateinit var exceptionsStore: ExceptionsFragmentStore + @MockK(relaxed = true) private lateinit var trackingProtectionUseCases: TrackingProtectionUseCases + private lateinit var interactor: TrackingProtectionExceptionsInteractor + private lateinit var onResult: CapturingSlot<(List) -> Unit> + + @Before + fun setup() { + MockKAnnotations.init(this) + interactor = DefaultTrackingProtectionExceptionsInteractor( + activity = activity, + exceptionsStore = exceptionsStore, + trackingProtectionUseCases = trackingProtectionUseCases + ) + + onResult = slot() + every { trackingProtectionUseCases.fetchExceptions(capture(onResult)) } just Runs + } + + @Test + fun onLearnMore() { + interactor.onLearnMore() + + val supportUrl = SupportUtils.getGenericSumoURLForTopic( + SupportUtils.SumoTopic.TRACKING_PROTECTION + ) + verify { + activity.openToBrowserAndLoad( + searchTermOrURL = supportUrl, + newTab = true, + from = BrowserDirection.FromTrackingProtectionExceptions + ) + } + } + + @Test + fun onDeleteAll() { + interactor.onDeleteAll() + verifySequence { + trackingProtectionUseCases.removeAllExceptions() + trackingProtectionUseCases.fetchExceptions(any()) + } + + val results = mockk>() + onResult.captured(results) + verify { exceptionsStore.dispatch(ExceptionsFragmentAction.Change(results)) } + } + + @Test + fun onDeleteOne() { + val exceptionsItem = mockk() + interactor.onDeleteOne(exceptionsItem) + verifySequence { + trackingProtectionUseCases.removeException(exceptionsItem) + trackingProtectionUseCases.fetchExceptions(any()) + } + + val results = mockk>() + onResult.captured(results) + verify { exceptionsStore.dispatch(ExceptionsFragmentAction.Change(results)) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsViewTest.kt b/app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsViewTest.kt similarity index 71% rename from app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsViewTest.kt rename to app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsViewTest.kt index a69c067b9..fbb41eca8 100644 --- a/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsViewTest.kt +++ b/app/src/test/java/org/mozilla/fenix/exceptions/trackingprotection/TrackingProtectionExceptionsViewTest.kt @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.fenix.trackingprotectionexceptions +package org.mozilla.fenix.exceptions.trackingprotection import android.text.Spannable import android.text.method.LinkMovementMethod @@ -29,25 +29,28 @@ import org.junit.runner.RunWith import org.mozilla.fenix.helpers.FenixRobolectricTestRunner @RunWith(FenixRobolectricTestRunner::class) -class ExceptionsViewTest { +class TrackingProtectionExceptionsViewTest { private lateinit var container: ViewGroup - private lateinit var interactor: ExceptionsInteractor - private lateinit var exceptionsView: ExceptionsView + private lateinit var interactor: TrackingProtectionExceptionsInteractor + private lateinit var exceptionsView: TrackingProtectionExceptionsView @Before fun setup() { - mockkConstructor(ExceptionsAdapter::class) + mockkConstructor(TrackingProtectionExceptionsAdapter::class) container = FrameLayout(testContext) interactor = mockk() - exceptionsView = ExceptionsView(container, interactor) - every { anyConstructed().updateData(any()) } just Runs + exceptionsView = TrackingProtectionExceptionsView( + container, + interactor + ) + every { anyConstructed().updateData(any()) } just Runs } @After fun teardown() { - unmockkConstructor(ExceptionsAdapter::class) + unmockkConstructor(TrackingProtectionExceptionsAdapter::class) } @Test @@ -63,20 +66,20 @@ class ExceptionsViewTest { @Test fun `binds empty list to adapter`() { - exceptionsView.update(ExceptionsFragmentState(emptyList())) + exceptionsView.update(emptyList()) assertTrue(exceptionsView.exceptions_empty_view.isVisible) assertFalse(exceptionsView.exceptions_list.isVisible) - verify { anyConstructed().updateData(emptyList()) } + verify { anyConstructed().updateData(emptyList()) } } @Test fun `binds list with items to adapter`() { val items = listOf(mockk(), mockk()) - exceptionsView.update(ExceptionsFragmentState(items)) + exceptionsView.update(items) assertFalse(exceptionsView.exceptions_empty_view.isVisible) assertTrue(exceptionsView.exceptions_list.isVisible) - verify { anyConstructed().updateData(items) } + verify { anyConstructed().updateData(items) } } } diff --git a/app/src/test/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsDeleteButtonViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsDeleteButtonViewHolderTest.kt similarity index 59% rename from app/src/test/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsDeleteButtonViewHolderTest.kt rename to app/src/test/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsDeleteButtonViewHolderTest.kt index 35721e5b0..7996b95ba 100644 --- a/app/src/test/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsDeleteButtonViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsDeleteButtonViewHolderTest.kt @@ -2,12 +2,14 @@ * 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.loginexceptions.viewholders +package org.mozilla.fenix.exceptions.viewholders import android.view.View import com.google.android.material.button.MaterialButton +import io.mockk.MockKAnnotations import io.mockk.Runs import io.mockk.every +import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk import io.mockk.slot @@ -15,28 +17,25 @@ import io.mockk.verify import org.junit.Before import org.junit.Test import org.mozilla.fenix.R -import org.mozilla.fenix.loginexceptions.LoginExceptionsInteractor +import org.mozilla.fenix.exceptions.ExceptionsInteractor -class LoginExceptionsDeleteButtonViewHolderTest { +class ExceptionsDeleteButtonViewHolderTest { - private lateinit var view: View - private lateinit var deleteButton: MaterialButton - private lateinit var interactor: LoginExceptionsInteractor + @MockK private lateinit var view: View + @MockK private lateinit var deleteButton: MaterialButton + @MockK private lateinit var interactor: ExceptionsInteractor @Before fun setup() { - deleteButton = mockk() - view = mockk { - every { findViewById(R.id.removeAllExceptions) } returns deleteButton - } - interactor = mockk() + MockKAnnotations.init(this) + every { view.findViewById(R.id.removeAllExceptions) } returns deleteButton } @Test fun `delete button calls interactor`() { val slot = slot() every { deleteButton.setOnClickListener(capture(slot)) } just Runs - LoginExceptionsDeleteButtonViewHolder(view, interactor) + ExceptionsDeleteButtonViewHolder(view, interactor) every { interactor.onDeleteAll() } just Runs slot.captured.onClick(mockk()) diff --git a/app/src/test/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsHeaderViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsHeaderViewHolderTest.kt similarity index 84% rename from app/src/test/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsHeaderViewHolderTest.kt rename to app/src/test/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsHeaderViewHolderTest.kt index 08121d22e..85e4d8415 100644 --- a/app/src/test/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsHeaderViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsHeaderViewHolderTest.kt @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.fenix.loginexceptions.viewholders +package org.mozilla.fenix.exceptions.viewholders import android.view.View import android.widget.TextView @@ -13,7 +13,7 @@ import org.junit.Before import org.junit.Test import org.mozilla.fenix.R -class LoginExceptionsHeaderViewHolderTest { +class ExceptionsHeaderViewHolderTest { private lateinit var view: View private lateinit var description: TextView @@ -31,7 +31,7 @@ class LoginExceptionsHeaderViewHolderTest { @Test fun `sets description text`() { - LoginExceptionsHeaderViewHolder(view) + ExceptionsHeaderViewHolder(view, R.string.preferences_passwords_exceptions_description) verify { description.text = "Logins and passwords will not be saved for these sites." } } } diff --git a/app/src/test/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsListItemViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsListItemViewHolderTest.kt new file mode 100644 index 000000000..e0cc1ed94 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsListItemViewHolderTest.kt @@ -0,0 +1,74 @@ +/* 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.exceptions.viewholders + +import android.view.View +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import mozilla.components.browser.icons.BrowserIcons +import mozilla.components.browser.icons.IconRequest +import org.junit.Before +import org.junit.Test +import org.mozilla.fenix.R +import org.mozilla.fenix.exceptions.ExceptionsInteractor + +class ExceptionsListItemViewHolderTest { + + @MockK private lateinit var view: View + @MockK(relaxUnitFun = true) private lateinit var url: TextView + @MockK(relaxUnitFun = true) private lateinit var deleteButton: ImageButton + @MockK private lateinit var favicon: ImageView + @MockK private lateinit var icons: BrowserIcons + @MockK private lateinit var interactor: ExceptionsInteractor + + @Before + fun setup() { + MockKAnnotations.init(this) + + every { view.findViewById(R.id.webAddressView) } returns url + every { view.findViewById(R.id.delete_exception) } returns deleteButton + every { view.findViewById(R.id.favicon_image) } returns favicon + every { icons.loadIntoView(favicon, any()) } returns mockk() + } + + @Test + fun `sets url text and loads favicon - mozilla`() { + ExceptionsListItemViewHolder(view, interactor, icons) + .bind(Exception(), url = "mozilla.org") + verify { url.text = "mozilla.org" } + verify { icons.loadIntoView(favicon, IconRequest("mozilla.org")) } + } + + @Test + fun `sets url text and loads favicon - example`() { + ExceptionsListItemViewHolder(view, interactor, icons) + .bind(Exception(), url = "https://example.com/icon.svg") + verify { url.text = "https://example.com/icon.svg" } + verify { icons.loadIntoView(favicon, IconRequest("https://example.com/icon.svg")) } + } + + @Test + fun `delete button calls interactor`() { + val slot = slot() + val exception = Exception() + every { deleteButton.setOnClickListener(capture(slot)) } just Runs + ExceptionsListItemViewHolder(view, interactor, icons).bind(exception, url = "mozilla.org") + + every { interactor.onDeleteOne(exception) } just Runs + slot.captured.onClick(mockk()) + verify { interactor.onDeleteOne(exception) } + } + + class Exception +} diff --git a/app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionsAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionsAdapterTest.kt deleted file mode 100644 index cada0d946..000000000 --- a/app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionsAdapterTest.kt +++ /dev/null @@ -1,113 +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.loginexceptions - -import android.widget.LinearLayout -import androidx.appcompat.view.ContextThemeWrapper -import io.mockk.every -import io.mockk.mockk -import mozilla.components.support.test.robolectric.testContext -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mozilla.fenix.R -import org.mozilla.fenix.helpers.FenixRobolectricTestRunner -import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsDeleteButtonViewHolder -import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsHeaderViewHolder -import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsListItemViewHolder - -@RunWith(FenixRobolectricTestRunner::class) -class LoginExceptionsAdapterTest { - - private lateinit var interactor: LoginExceptionsInteractor - private lateinit var adapter: LoginExceptionsAdapter - - @Before - fun setup() { - interactor = mockk() - adapter = LoginExceptionsAdapter(interactor) - } - - @Test - fun `creates correct view holder type`() { - val parent = LinearLayout(ContextThemeWrapper(testContext, R.style.NormalTheme)) - adapter.updateData(listOf(mockk(), mockk())) - assertEquals(4, adapter.itemCount) - - val holders = (0 until adapter.itemCount).asSequence() - .map { i -> adapter.getItemViewType(i) } - .map { viewType -> adapter.onCreateViewHolder(parent, viewType) } - .toList() - assertEquals(4, holders.size) - - assertTrue(holders[0] is LoginExceptionsHeaderViewHolder) - assertTrue(holders[1] is LoginExceptionsListItemViewHolder) - assertTrue(holders[2] is LoginExceptionsListItemViewHolder) - assertTrue(holders[3] is LoginExceptionsDeleteButtonViewHolder) - } - - @Test - fun `headers and delete should check if the other object is the same`() { - assertTrue(LoginExceptionsAdapter.DiffCallback.areItemsTheSame( - LoginExceptionsAdapter.AdapterItem.Header, - LoginExceptionsAdapter.AdapterItem.Header - )) - assertTrue(LoginExceptionsAdapter.DiffCallback.areItemsTheSame( - LoginExceptionsAdapter.AdapterItem.DeleteButton, - LoginExceptionsAdapter.AdapterItem.DeleteButton - )) - assertFalse(LoginExceptionsAdapter.DiffCallback.areItemsTheSame( - LoginExceptionsAdapter.AdapterItem.Header, - LoginExceptionsAdapter.AdapterItem.DeleteButton - )) - assertTrue(LoginExceptionsAdapter.DiffCallback.areContentsTheSame( - LoginExceptionsAdapter.AdapterItem.Header, - LoginExceptionsAdapter.AdapterItem.Header - )) - assertTrue(LoginExceptionsAdapter.DiffCallback.areContentsTheSame( - LoginExceptionsAdapter.AdapterItem.DeleteButton, - LoginExceptionsAdapter.AdapterItem.DeleteButton - )) - assertFalse(LoginExceptionsAdapter.DiffCallback.areContentsTheSame( - LoginExceptionsAdapter.AdapterItem.DeleteButton, - LoginExceptionsAdapter.AdapterItem.Header - )) - } - - @Test - fun `items with the same id should be marked as same`() { - assertTrue(LoginExceptionsAdapter.DiffCallback.areItemsTheSame( - LoginExceptionsAdapter.AdapterItem.Item(mockk { - every { id } returns 12L - }), - LoginExceptionsAdapter.AdapterItem.Item(mockk { - every { id } returns 12L - }) - )) - assertFalse(LoginExceptionsAdapter.DiffCallback.areItemsTheSame( - LoginExceptionsAdapter.AdapterItem.Item(mockk { - every { id } returns 14L - }), - LoginExceptionsAdapter.AdapterItem.Item(mockk { - every { id } returns 12L - }) - )) - assertFalse(LoginExceptionsAdapter.DiffCallback.areItemsTheSame( - LoginExceptionsAdapter.AdapterItem.Item(mockk { - every { id } returns 14L - }), - LoginExceptionsAdapter.AdapterItem.Header - )) - assertFalse(LoginExceptionsAdapter.DiffCallback.areItemsTheSame( - LoginExceptionsAdapter.AdapterItem.DeleteButton, - LoginExceptionsAdapter.AdapterItem.Item(mockk { - every { id } returns 14L - }) - )) - } -} diff --git a/app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionsInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionsInteractorTest.kt deleted file mode 100644 index 309f3e681..000000000 --- a/app/src/test/java/org/mozilla/fenix/loginexceptions/LoginExceptionsInteractorTest.kt +++ /dev/null @@ -1,36 +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.loginexceptions - -import io.mockk.mockk -import mozilla.components.feature.logins.exceptions.LoginException -import org.junit.Assert.assertEquals -import org.junit.Test - -class LoginExceptionsInteractorTest { - - @Test - fun onDeleteAll() { - var onDeleteAll = false - val interactor = LoginExceptionsInteractor( - mockk(), - { onDeleteAll = true } - ) - interactor.onDeleteAll() - assertEquals(true, onDeleteAll) - } - - @Test - fun onDeleteOne() { - var exceptionsItemReceived: LoginException? = null - val exceptionsItem: LoginException = mockk() - val interactor = LoginExceptionsInteractor( - { exceptionsItemReceived = exceptionsItem }, - mockk() - ) - interactor.onDeleteOne(exceptionsItem) - assertEquals(exceptionsItemReceived, exceptionsItem) - } -} diff --git a/app/src/test/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsListItemViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsListItemViewHolderTest.kt deleted file mode 100644 index 1243f665b..000000000 --- a/app/src/test/java/org/mozilla/fenix/loginexceptions/viewholders/LoginExceptionsListItemViewHolderTest.kt +++ /dev/null @@ -1,63 +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.loginexceptions.viewholders - -import android.view.View -import android.widget.ImageButton -import android.widget.ImageView -import android.widget.TextView -import io.mockk.Runs -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify -import mozilla.components.feature.logins.exceptions.LoginException -import org.junit.Before -import org.junit.Test -import org.mozilla.fenix.R -import org.mozilla.fenix.loginexceptions.LoginExceptionsInteractor - -class LoginExceptionsListItemViewHolderTest { - - private lateinit var view: View - private lateinit var url: TextView - private lateinit var deleteButton: ImageButton - private lateinit var interactor: LoginExceptionsInteractor - - @Before - fun setup() { - url = mockk(relaxUnitFun = true) - deleteButton = mockk(relaxUnitFun = true) - view = mockk { - every { findViewById(R.id.webAddressView) } returns url - every { findViewById(R.id.delete_exception) } returns deleteButton - every { findViewById(R.id.favicon_image) } returns mockk() - } - interactor = mockk() - } - - @Test - fun `sets url text`() { - LoginExceptionsListItemViewHolder(view, interactor).bind(mockk { - every { origin } returns "mozilla.org" - }) - verify { url.text = "mozilla.org" } - } - - @Test - fun `delete button calls interactor`() { - val slot = slot() - val loginException = mockk { - every { origin } returns "mozilla.org" - } - every { deleteButton.setOnClickListener(capture(slot)) } just Runs - LoginExceptionsListItemViewHolder(view, interactor).bind(loginException) - - every { interactor.onDeleteOne(loginException) } just Runs - slot.captured.onClick(mockk()) - verify { interactor.onDeleteOne(loginException) } - } -} diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsAdapterTest.kt deleted file mode 100644 index 10fff5ab3..000000000 --- a/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsAdapterTest.kt +++ /dev/null @@ -1,42 +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.trackingprotectionexceptions - -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mozilla.fenix.helpers.FenixRobolectricTestRunner -import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsDeleteButtonViewHolder -import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsHeaderViewHolder -import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsListItemViewHolder - -@ExperimentalCoroutinesApi -@RunWith(FenixRobolectricTestRunner::class) -class ExceptionsAdapterTest { - - private lateinit var interactor: ExceptionsInteractor - private lateinit var adapter: ExceptionsAdapter - - @Before - fun setup() { - interactor = mockk() - adapter = ExceptionsAdapter(interactor) - } - - @Test - fun `binds header and delete button with other adapter items`() = runBlockingTest { - adapter.updateData(listOf(mockk(), mockk())) - - assertEquals(4, adapter.itemCount) - assertEquals(ExceptionsHeaderViewHolder.LAYOUT_ID, adapter.getItemViewType(0)) - assertEquals(ExceptionsListItemViewHolder.LAYOUT_ID, adapter.getItemViewType(1)) - assertEquals(ExceptionsListItemViewHolder.LAYOUT_ID, adapter.getItemViewType(2)) - assertEquals(ExceptionsDeleteButtonViewHolder.LAYOUT_ID, adapter.getItemViewType(3)) - } -} diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsInteractorTest.kt deleted file mode 100644 index 84cfa928d..000000000 --- a/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsInteractorTest.kt +++ /dev/null @@ -1,54 +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.trackingprotectionexceptions - -import io.mockk.mockk -import mozilla.components.concept.engine.content.blocking.TrackingProtectionException -import org.junit.Assert.assertEquals -import org.junit.Test - -class ExceptionsInteractorTest { - - @Test - fun onLearnMore() { - var learnMoreClicked = false - val interactor = - ExceptionsInteractor( - { learnMoreClicked = true }, - mockk(), - mockk() - ) - interactor.onLearnMore() - assertEquals(true, learnMoreClicked) - } - - @Test - fun onDeleteAll() { - var onDeleteAll = false - val interactor = - ExceptionsInteractor( - mockk(), - mockk(), - { onDeleteAll = true } - ) - interactor.onDeleteAll() - assertEquals(true, onDeleteAll) - } - - @Test - fun onDeleteOne() { - var exceptionsItemReceived: TrackingProtectionException? = null - val exceptionsItem = - ExceptionItem("url") - val interactor = - ExceptionsInteractor( - mockk(), - { exceptionsItemReceived = exceptionsItem }, - mockk() - ) - interactor.onDeleteOne(exceptionsItem) - assertEquals(exceptionsItemReceived, exceptionsItem) - } -} diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/TrackingProtectionExceptionsFragmentStoreTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/TrackingProtectionExceptionsFragmentStoreTest.kt deleted file mode 100644 index 027f37058..000000000 --- a/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/TrackingProtectionExceptionsFragmentStoreTest.kt +++ /dev/null @@ -1,35 +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.trackingprotectionexceptions - -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotSame -import org.junit.Test - -class TrackingProtectionExceptionsFragmentStoreTest { - @Test - fun onChange() = runBlocking { - val initialState = emptyDefaultState() - val store = - ExceptionsFragmentStore( - initialState - ) - val newExceptionsItem = - ExceptionItem("URL") - - store.dispatch(ExceptionsFragmentAction.Change(listOf(newExceptionsItem))).join() - assertNotSame(initialState, store.state) - assertEquals( - store.state.items, - listOf(newExceptionsItem) - ) - } - - private fun emptyDefaultState(): ExceptionsFragmentState = - ExceptionsFragmentState( - items = listOf() - ) -} diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsDeleteButtonViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsDeleteButtonViewHolderTest.kt deleted file mode 100644 index 8c53e28ff..000000000 --- a/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsDeleteButtonViewHolderTest.kt +++ /dev/null @@ -1,43 +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.trackingprotectionexceptions.viewholders - -import android.view.LayoutInflater -import android.view.View -import androidx.appcompat.view.ContextThemeWrapper -import io.mockk.mockk -import io.mockk.verify -import kotlinx.android.synthetic.main.delete_exceptions_button.view.* -import mozilla.components.support.test.robolectric.testContext -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mozilla.fenix.R -import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsInteractor -import org.mozilla.fenix.helpers.FenixRobolectricTestRunner - -@RunWith(FenixRobolectricTestRunner::class) -class ExceptionsDeleteButtonViewHolderTest { - - private lateinit var view: View - private lateinit var interactor: ExceptionsInteractor - private lateinit var viewHolder: ExceptionsDeleteButtonViewHolder - - @Before - fun setup() { - val appCompatContext = ContextThemeWrapper(testContext, R.style.NormalTheme) - view = LayoutInflater.from(appCompatContext) - .inflate(ExceptionsDeleteButtonViewHolder.LAYOUT_ID, null) - interactor = mockk(relaxed = true) - viewHolder = ExceptionsDeleteButtonViewHolder(view, interactor) - } - - @Test - fun `calls onDeleteAll on click`() { - view.removeAllExceptions.performClick() - - verify { interactor.onDeleteAll() } - } -} diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsListItemViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsListItemViewHolderTest.kt deleted file mode 100644 index 3efb3d907..000000000 --- a/app/src/test/java/org/mozilla/fenix/trackingprotectionexceptions/viewholders/ExceptionsListItemViewHolderTest.kt +++ /dev/null @@ -1,56 +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.trackingprotectionexceptions.viewholders - -import android.view.LayoutInflater -import android.view.View -import io.mockk.mockk -import io.mockk.verify -import kotlinx.android.synthetic.main.exception_item.view.* -import mozilla.components.concept.engine.content.blocking.TrackingProtectionException -import mozilla.components.support.test.robolectric.testContext -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsInteractor -import org.mozilla.fenix.helpers.FenixRobolectricTestRunner - -@RunWith(FenixRobolectricTestRunner::class) -class ExceptionsListItemViewHolderTest { - - private lateinit var view: View - private lateinit var interactor: ExceptionsInteractor - private lateinit var viewHolder: ExceptionsListItemViewHolder - - @Before - fun setup() { - view = LayoutInflater.from(testContext) - .inflate(ExceptionsListItemViewHolder.LAYOUT_ID, null) - interactor = mockk(relaxed = true) - viewHolder = ExceptionsListItemViewHolder(view, interactor) - } - - @Test - fun `bind url and icon`() { - val exception = object : TrackingProtectionException { - override val url = "https://example.com/icon.svg" - } - viewHolder.bind(exception) - - assertEquals(exception.url, view.webAddressView.text) - } - - @Test - fun `calls onDeleteOne on click`() { - val exception = object : TrackingProtectionException { - override val url = "https://example.com/icon.svg" - } - viewHolder.bind(exception) - view.delete_exception.performClick() - - verify { interactor.onDeleteOne(exception) } - } -} From f14b6d0385b8b5a43557d33e59f3a9a34a6cb080 Mon Sep 17 00:00:00 2001 From: ekager Date: Fri, 31 Jul 2020 14:21:00 -0400 Subject: [PATCH 56/74] For #13117 - Don't show add to collections button in private tabs tray --- .../tabtray/SaveToCollectionsButtonAdapter.kt | 20 ++++++++++++++----- .../org/mozilla/fenix/tabtray/TabTrayView.kt | 13 ++++++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/SaveToCollectionsButtonAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabtray/SaveToCollectionsButtonAdapter.kt index 896fdea7d..03d10be28 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/SaveToCollectionsButtonAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/SaveToCollectionsButtonAdapter.kt @@ -20,7 +20,8 @@ import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.ViewHolder * multiple [RecyclerView.Adapter] in one [RecyclerView]. */ class SaveToCollectionsButtonAdapter( - private val interactor: TabTrayInteractor + private val interactor: TabTrayInteractor, + private val isPrivate: Boolean = false ) : ListAdapter(DiffCallback) { init { @@ -38,14 +39,19 @@ class SaveToCollectionsButtonAdapter( return } - (payloads[0] as TabTrayView.TabChange).let { - holder.itemView.isVisible = it == TabTrayView.TabChange.NORMAL + when (val change = payloads[0]) { + is TabTrayView.TabChange -> { + holder.itemView.isVisible = change == TabTrayView.TabChange.NORMAL + } + is MultiselectModeChange -> { + holder.itemView.isVisible = change == MultiselectModeChange.NORMAL + } } } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.itemView.isVisible = - interactor.onModeRequested() is TabTrayDialogFragmentState.Mode.Normal + holder.itemView.isVisible = !isPrivate && + interactor.onModeRequested() is TabTrayDialogFragmentState.Mode.Normal } override fun getItemViewType(position: Int): Int { @@ -58,6 +64,10 @@ class SaveToCollectionsButtonAdapter( override fun areContentsTheSame(oldItem: Item, newItem: Item) = true } + enum class MultiselectModeChange { + MULTISELECT, NORMAL + } + /** * An object to identify the data type. */ 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 f11930604..d0d903447 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt @@ -41,6 +41,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.MultiselectModeChange /** * View that contains and configures the BrowserAwesomeBar @@ -72,7 +73,7 @@ class TabTrayView( private val bottomSheetCallback: BottomSheetBehavior.BottomSheetCallback private var tabsTouchHelper: TabsTouchHelper - private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor) + private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate) private var hasLoaded = false @@ -272,7 +273,7 @@ class TabTrayView( val oldMode = mode if (oldMode::class != state.mode::class) { - updateTabsForModeChanged() + updateTabsForMultiselectModeChanged(state.mode is TabTrayDialogFragmentState.Mode.MultiSelect) if (view.context.settings().accessibilityServicesEnabled) { view.announceForAccessibility( if (state.mode == TabTrayDialogFragmentState.Mode.Normal) view.context.getString( @@ -419,13 +420,17 @@ class TabTrayView( view.tab_layout.isVisible = !multiselect } - private fun updateTabsForModeChanged() { + private fun updateTabsForMultiselectModeChanged(inMultiselectMode: Boolean) { view.tabsTray.apply { val tabs = view.context.components.core.store.state.getNormalOrPrivateTabs( isPrivateModeSelected ) - collectionsButtonAdapter.notifyItemChanged(0) + collectionsButtonAdapter.notifyItemChanged( + 0, + if (inMultiselectMode) MultiselectModeChange.MULTISELECT else MultiselectModeChange.NORMAL + ) + tabsAdapter.notifyItemRangeChanged(0, tabs.size, true) } } From e1653f629ed5eed2482f383a0b79e4e9e908147f Mon Sep 17 00:00:00 2001 From: Kainalu Hagiwara Date: Thu, 30 Jul 2020 11:24:14 -0700 Subject: [PATCH 57/74] For #13127 - Make sure tabPreview is added after browserLayout. --- .../java/org/mozilla/fenix/browser/BaseBrowserFragment.kt | 4 +--- app/src/main/res/layout/browser_gesture_wrapper.xml | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) 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 54f0ccb2d..d16f956e9 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -167,9 +167,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session customTabSessionId = arguments?.getString(EXTRA_SESSION_ID) val view = if (FeatureFlags.browserChromeGestures) { - inflater.inflate(R.layout.browser_gesture_wrapper, container, false).apply { - inflater.inflate(R.layout.fragment_browser, this as SwipeGestureLayout, true) - } + inflater.inflate(R.layout.browser_gesture_wrapper, container, false) } else { inflater.inflate(R.layout.fragment_browser, container, false) } diff --git a/app/src/main/res/layout/browser_gesture_wrapper.xml b/app/src/main/res/layout/browser_gesture_wrapper.xml index 94f515687..4de55e61f 100644 --- a/app/src/main/res/layout/browser_gesture_wrapper.xml +++ b/app/src/main/res/layout/browser_gesture_wrapper.xml @@ -7,6 +7,8 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + Date: Wed, 29 Jul 2020 16:06:23 -0700 Subject: [PATCH 58/74] Move MetricController to its own file --- .../components/metrics/MetricController.kt | 227 ++++++++++++++++++ .../fenix/components/metrics/Metrics.kt | 220 ----------------- 2 files changed, 227 insertions(+), 220 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt new file mode 100644 index 000000000..3b7c5bb5c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt @@ -0,0 +1,227 @@ +/* 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 androidx.annotation.VisibleForTesting +import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts +import mozilla.components.browser.menu.facts.BrowserMenuFacts +import mozilla.components.browser.toolbar.facts.ToolbarFacts +import mozilla.components.concept.awesomebar.AwesomeBar +import mozilla.components.feature.awesomebar.provider.BookmarksStorageSuggestionProvider +import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider +import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider +import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider +import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider +import mozilla.components.feature.contextmenu.facts.ContextMenuFacts +import mozilla.components.feature.customtabs.CustomTabsFacts +import mozilla.components.feature.downloads.facts.DownloadsFacts +import mozilla.components.feature.findinpage.facts.FindInPageFacts +import mozilla.components.feature.media.facts.MediaFacts +import mozilla.components.feature.prompts.dialog.LoginDialogFacts +import mozilla.components.support.base.Component +import mozilla.components.support.base.facts.Action +import mozilla.components.support.base.facts.Fact +import mozilla.components.support.base.facts.FactProcessor +import mozilla.components.support.base.facts.Facts +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.webextensions.facts.WebExtensionFacts +import org.mozilla.fenix.BuildConfig +import org.mozilla.fenix.GleanMetrics.Addons +import org.mozilla.fenix.GleanMetrics.PerfAwesomebar +import org.mozilla.fenix.search.awesomebar.ShortcutsSuggestionProvider + +interface MetricController { + fun start(type: MetricServiceType) + fun stop(type: MetricServiceType) + fun track(event: Event) + + companion object { + fun create( + services: List, + isDataTelemetryEnabled: () -> Boolean, + isMarketingDataTelemetryEnabled: () -> Boolean + ): MetricController { + return if (BuildConfig.TELEMETRY) { + ReleaseMetricController( + services, + isDataTelemetryEnabled, + isMarketingDataTelemetryEnabled + ) + } else DebugMetricController() + } + } +} + +@VisibleForTesting +internal class DebugMetricController( + private val logger: Logger = Logger() +) : MetricController { + + override fun start(type: MetricServiceType) { + logger.debug("DebugMetricController: start") + } + + override fun stop(type: MetricServiceType) { + logger.debug("DebugMetricController: stop") + } + + override fun track(event: Event) { + logger.debug("DebugMetricController: track event: $event") + } +} + +@VisibleForTesting +internal class ReleaseMetricController( + private val services: List, + private val isDataTelemetryEnabled: () -> Boolean, + private val isMarketingDataTelemetryEnabled: () -> Boolean +) : MetricController { + private var initialized = mutableSetOf() + + init { + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + fact.toEvent()?.also { + track(it) + } + } + }) + } + + override fun start(type: MetricServiceType) { + val isEnabled = isTelemetryEnabled(type) + val isInitialized = isInitialized(type) + if (!isEnabled || isInitialized) { + return + } + + services + .filter { it.type == type } + .forEach { it.start() } + + initialized.add(type) + } + + override fun stop(type: MetricServiceType) { + val isEnabled = isTelemetryEnabled(type) + val isInitialized = isInitialized(type) + if (isEnabled || !isInitialized) { + return + } + + services + .filter { it.type == type } + .forEach { it.stop() } + + initialized.remove(type) + } + + override fun track(event: Event) { + services + .filter { it.shouldTrack(event) } + .forEach { + val isEnabled = isTelemetryEnabled(it.type) + val isInitialized = isInitialized(it.type) + if (!isEnabled || !isInitialized) { + return@forEach + } + + it.track(event) + } + } + + private fun isInitialized(type: MetricServiceType): Boolean = initialized.contains(type) + + private fun isTelemetryEnabled(type: MetricServiceType): Boolean = when (type) { + MetricServiceType.Data -> isDataTelemetryEnabled() + MetricServiceType.Marketing -> isMarketingDataTelemetryEnabled() + } + + private fun Fact.toEvent(): Event? = when (Pair(component, item)) { + Component.FEATURE_PROMPTS to LoginDialogFacts.Items.DISPLAY -> Event.LoginDialogPromptDisplayed + Component.FEATURE_PROMPTS to LoginDialogFacts.Items.CANCEL -> Event.LoginDialogPromptCancelled + Component.FEATURE_PROMPTS to LoginDialogFacts.Items.NEVER_SAVE -> Event.LoginDialogPromptNeverSave + Component.FEATURE_PROMPTS to LoginDialogFacts.Items.SAVE -> Event.LoginDialogPromptSave + + Component.FEATURE_FINDINPAGE to FindInPageFacts.Items.CLOSE -> Event.FindInPageClosed + Component.FEATURE_FINDINPAGE to FindInPageFacts.Items.INPUT -> Event.FindInPageSearchCommitted + Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.ITEM -> { + metadata?.get("item")?.let { Event.ContextMenuItemTapped.create(it.toString()) } + } + + Component.BROWSER_TOOLBAR to ToolbarFacts.Items.MENU -> { + metadata?.get("customTab")?.let { Event.CustomTabsMenuOpened } + } + Component.BROWSER_MENU to BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM -> { + metadata?.get("id")?.let { Event.AddonsOpenInToolbarMenu(it.toString()) } + } + Component.FEATURE_CUSTOMTABS to CustomTabsFacts.Items.CLOSE -> Event.CustomTabsClosed + Component.FEATURE_CUSTOMTABS to CustomTabsFacts.Items.ACTION_BUTTON -> Event.CustomTabsActionTapped + + Component.FEATURE_DOWNLOADS to DownloadsFacts.Items.NOTIFICATION -> { + when (action) { + Action.CANCEL -> Event.NotificationDownloadCancel + Action.OPEN -> Event.NotificationDownloadOpen + Action.PAUSE -> Event.NotificationDownloadPause + Action.RESUME -> Event.NotificationDownloadResume + Action.TRY_AGAIN -> Event.NotificationDownloadTryAgain + else -> null + } + } + + Component.FEATURE_MEDIA to MediaFacts.Items.NOTIFICATION -> { + when (action) { + Action.PLAY -> Event.NotificationMediaPlay + Action.PAUSE -> Event.NotificationMediaPause + else -> null + } + } + Component.FEATURE_MEDIA to MediaFacts.Items.STATE -> { + when (action) { + Action.PLAY -> Event.MediaPlayState + Action.PAUSE -> Event.MediaPauseState + Action.STOP -> Event.MediaStopState + else -> null + } + } + Component.SUPPORT_WEBEXTENSIONS to WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED -> { + metadata?.get("installed")?.let { installedAddons -> + if (installedAddons is List<*>) { + Addons.installedAddons.set(installedAddons.map { it.toString() }) + Addons.hasInstalledAddons.set(installedAddons.size > 0) + } + } + + metadata?.get("enabled")?.let { enabledAddons -> + if (enabledAddons is List<*>) { + Addons.enabledAddons.set(enabledAddons.map { it.toString() }) + Addons.hasEnabledAddons.set(enabledAddons.size > 0) + } + } + + null + } + Component.BROWSER_AWESOMEBAR to BrowserAwesomeBarFacts.Items.PROVIDER_DURATION -> { + metadata?.get(BrowserAwesomeBarFacts.MetadataKeys.DURATION_PAIR)?.let { providerTiming -> + require(providerTiming is Pair<*, *>) { "Expected providerTiming to be a Pair" } + when (val provider = providerTiming.first as AwesomeBar.SuggestionProvider) { + is HistoryStorageSuggestionProvider -> PerfAwesomebar.historySuggestions + is BookmarksStorageSuggestionProvider -> PerfAwesomebar.bookmarkSuggestions + is SessionSuggestionProvider -> PerfAwesomebar.sessionSuggestions + is SearchSuggestionProvider -> PerfAwesomebar.searchEngineSuggestions + is ClipboardSuggestionProvider -> PerfAwesomebar.clipboardSuggestions + is ShortcutsSuggestionProvider -> PerfAwesomebar.shortcutsSuggestions + // NB: add PerfAwesomebar.syncedTabsSuggestions once we're using SyncedTabsSuggestionProvider + else -> { + Logger("Metrics").error("Unknown suggestion provider: $provider") + null + } + }?.accumulateSamples(longArrayOf(providerTiming.second as Long)) + } + null + } + else -> null + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt index 93ce11cc7..26a408340 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt @@ -5,32 +5,8 @@ package org.mozilla.fenix.components.metrics import android.content.Context -import androidx.annotation.VisibleForTesting -import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts import mozilla.components.browser.errorpages.ErrorType -import mozilla.components.browser.menu.facts.BrowserMenuFacts import mozilla.components.browser.search.SearchEngine -import mozilla.components.browser.toolbar.facts.ToolbarFacts -import mozilla.components.concept.awesomebar.AwesomeBar -import mozilla.components.feature.awesomebar.provider.BookmarksStorageSuggestionProvider -import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider -import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider -import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider -import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider -import mozilla.components.feature.contextmenu.facts.ContextMenuFacts -import mozilla.components.feature.customtabs.CustomTabsFacts -import mozilla.components.feature.downloads.facts.DownloadsFacts -import mozilla.components.feature.findinpage.facts.FindInPageFacts -import mozilla.components.feature.media.facts.MediaFacts -import mozilla.components.feature.prompts.dialog.LoginDialogFacts -import mozilla.components.support.base.Component -import mozilla.components.support.base.facts.Action -import mozilla.components.support.base.facts.Fact -import mozilla.components.support.base.facts.FactProcessor -import mozilla.components.support.base.facts.Facts -import mozilla.components.support.base.log.logger.Logger -import mozilla.components.support.webextensions.facts.WebExtensionFacts -import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.GleanMetrics.AppTheme import org.mozilla.fenix.GleanMetrics.Autoplay @@ -41,13 +17,11 @@ import org.mozilla.fenix.GleanMetrics.ErrorPage import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.GleanMetrics.Onboarding -import org.mozilla.fenix.GleanMetrics.PerfAwesomebar import org.mozilla.fenix.GleanMetrics.SearchShortcuts import org.mozilla.fenix.GleanMetrics.Tip import org.mozilla.fenix.GleanMetrics.ToolbarSettings import org.mozilla.fenix.GleanMetrics.TrackingProtection import org.mozilla.fenix.R -import org.mozilla.fenix.search.awesomebar.ShortcutsSuggestionProvider import java.util.Locale sealed class Event { @@ -532,92 +506,6 @@ sealed class Event { get() = null } -private fun Fact.toEvent(): Event? = when (Pair(component, item)) { - Component.FEATURE_PROMPTS to LoginDialogFacts.Items.DISPLAY -> Event.LoginDialogPromptDisplayed - Component.FEATURE_PROMPTS to LoginDialogFacts.Items.CANCEL -> Event.LoginDialogPromptCancelled - Component.FEATURE_PROMPTS to LoginDialogFacts.Items.NEVER_SAVE -> Event.LoginDialogPromptNeverSave - Component.FEATURE_PROMPTS to LoginDialogFacts.Items.SAVE -> Event.LoginDialogPromptSave - - Component.FEATURE_FINDINPAGE to FindInPageFacts.Items.CLOSE -> Event.FindInPageClosed - Component.FEATURE_FINDINPAGE to FindInPageFacts.Items.INPUT -> Event.FindInPageSearchCommitted - Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.ITEM -> { - metadata?.get("item")?.let { Event.ContextMenuItemTapped.create(it.toString()) } - } - - Component.BROWSER_TOOLBAR to ToolbarFacts.Items.MENU -> { - metadata?.get("customTab")?.let { Event.CustomTabsMenuOpened } - } - Component.BROWSER_MENU to BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM -> { - metadata?.get("id")?.let { Event.AddonsOpenInToolbarMenu(it.toString()) } - } - Component.FEATURE_CUSTOMTABS to CustomTabsFacts.Items.CLOSE -> Event.CustomTabsClosed - Component.FEATURE_CUSTOMTABS to CustomTabsFacts.Items.ACTION_BUTTON -> Event.CustomTabsActionTapped - - Component.FEATURE_DOWNLOADS to DownloadsFacts.Items.NOTIFICATION -> { - when (action) { - Action.CANCEL -> Event.NotificationDownloadCancel - Action.OPEN -> Event.NotificationDownloadOpen - Action.PAUSE -> Event.NotificationDownloadPause - Action.RESUME -> Event.NotificationDownloadResume - Action.TRY_AGAIN -> Event.NotificationDownloadTryAgain - else -> null - } - } - - Component.FEATURE_MEDIA to MediaFacts.Items.NOTIFICATION -> { - when (action) { - Action.PLAY -> Event.NotificationMediaPlay - Action.PAUSE -> Event.NotificationMediaPause - else -> null - } - } - Component.FEATURE_MEDIA to MediaFacts.Items.STATE -> { - when (action) { - Action.PLAY -> Event.MediaPlayState - Action.PAUSE -> Event.MediaPauseState - Action.STOP -> Event.MediaStopState - else -> null - } - } - Component.SUPPORT_WEBEXTENSIONS to WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED -> { - metadata?.get("installed")?.let { installedAddons -> - if (installedAddons is List<*>) { - Addons.installedAddons.set(installedAddons.map { it.toString() }) - Addons.hasInstalledAddons.set(installedAddons.size > 0) - } - } - - metadata?.get("enabled")?.let { enabledAddons -> - if (enabledAddons is List<*>) { - Addons.enabledAddons.set(enabledAddons.map { it.toString() }) - Addons.hasEnabledAddons.set(enabledAddons.size > 0) - } - } - - null - } - Component.BROWSER_AWESOMEBAR to BrowserAwesomeBarFacts.Items.PROVIDER_DURATION -> { - metadata?.get(BrowserAwesomeBarFacts.MetadataKeys.DURATION_PAIR)?.let { providerTiming -> - require(providerTiming is Pair<*, *>) { "Expected providerTiming to be a Pair" } - when (val provider = providerTiming.first as AwesomeBar.SuggestionProvider) { - is HistoryStorageSuggestionProvider -> PerfAwesomebar.historySuggestions - is BookmarksStorageSuggestionProvider -> PerfAwesomebar.bookmarkSuggestions - is SessionSuggestionProvider -> PerfAwesomebar.sessionSuggestions - is SearchSuggestionProvider -> PerfAwesomebar.searchEngineSuggestions - is ClipboardSuggestionProvider -> PerfAwesomebar.clipboardSuggestions - is ShortcutsSuggestionProvider -> PerfAwesomebar.shortcutsSuggestions - // NB: add PerfAwesomebar.syncedTabsSuggestions once we're using SyncedTabsSuggestionProvider - else -> { - Logger("Metrics").error("Unknown suggestion provider: $provider") - null - } - }?.accumulateSamples(longArrayOf(providerTiming.second as Long)) - } - null - } - else -> null -} - enum class MetricServiceType { Data, Marketing; } @@ -630,111 +518,3 @@ interface MetricsService { fun track(event: Event) fun shouldTrack(event: Event): Boolean } - -interface MetricController { - fun start(type: MetricServiceType) - fun stop(type: MetricServiceType) - fun track(event: Event) - - companion object { - fun create( - services: List, - isDataTelemetryEnabled: () -> Boolean, - isMarketingDataTelemetryEnabled: () -> Boolean - ): MetricController { - return if (BuildConfig.TELEMETRY) { - ReleaseMetricController( - services, - isDataTelemetryEnabled, - isMarketingDataTelemetryEnabled - ) - } else DebugMetricController() - } - } -} - -@VisibleForTesting -internal class DebugMetricController( - private val logger: Logger = Logger() -) : MetricController { - - override fun start(type: MetricServiceType) { - logger.debug("DebugMetricController: start") - } - - override fun stop(type: MetricServiceType) { - logger.debug("DebugMetricController: stop") - } - - override fun track(event: Event) { - logger.debug("DebugMetricController: track event: $event") - } -} - -@VisibleForTesting -internal class ReleaseMetricController( - private val services: List, - private val isDataTelemetryEnabled: () -> Boolean, - private val isMarketingDataTelemetryEnabled: () -> Boolean -) : MetricController { - private var initialized = mutableSetOf() - - init { - Facts.registerProcessor(object : FactProcessor { - override fun process(fact: Fact) { - fact.toEvent()?.also { - track(it) - } - } - }) - } - - override fun start(type: MetricServiceType) { - val isEnabled = isTelemetryEnabled(type) - val isInitialized = isInitialized(type) - if (!isEnabled || isInitialized) { - return - } - - services - .filter { it.type == type } - .forEach { it.start() } - - initialized.add(type) - } - - override fun stop(type: MetricServiceType) { - val isEnabled = isTelemetryEnabled(type) - val isInitialized = isInitialized(type) - if (isEnabled || !isInitialized) { - return - } - - services - .filter { it.type == type } - .forEach { it.stop() } - - initialized.remove(type) - } - - override fun track(event: Event) { - services - .filter { it.shouldTrack(event) } - .forEach { - val isEnabled = isTelemetryEnabled(it.type) - val isInitialized = isInitialized(it.type) - if (!isEnabled || !isInitialized) { - return@forEach - } - - it.track(event) - } - } - - private fun isInitialized(type: MetricServiceType): Boolean = initialized.contains(type) - - private fun isTelemetryEnabled(type: MetricServiceType): Boolean = when (type) { - MetricServiceType.Data -> isDataTelemetryEnabled() - MetricServiceType.Marketing -> isMarketingDataTelemetryEnabled() - } -} From 661ffdfae21ae10f9e0cc14a69d7ff075e25d13f Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Wed, 29 Jul 2020 16:09:29 -0700 Subject: [PATCH 59/74] Moved MetricsService to its own file --- .../fenix/components/metrics/Metrics.kt | 12 ------------ .../fenix/components/metrics/MetricsService.kt | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/components/metrics/MetricsService.kt diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt index 26a408340..c22cec86b 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt @@ -506,15 +506,3 @@ sealed class Event { get() = null } -enum class MetricServiceType { - Data, Marketing; -} - -interface MetricsService { - val type: MetricServiceType - - fun start() - fun stop() - fun track(event: Event) - fun shouldTrack(event: Event): Boolean -} diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsService.kt new file mode 100644 index 000000000..90dc67785 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsService.kt @@ -0,0 +1,18 @@ +/* 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 + +enum class MetricServiceType { + Data, Marketing; +} + +interface MetricsService { + val type: MetricServiceType + + fun start() + fun stop() + fun track(event: Event) + fun shouldTrack(event: Event): Boolean +} From 70c66185d815b7d5a68f6b8e5bfc405a65bafc96 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Wed, 29 Jul 2020 16:11:51 -0700 Subject: [PATCH 60/74] Renamed Metrics to Event --- .../mozilla/fenix/components/metrics/{Metrics.kt => Event.kt} | 1 - 1 file changed, 1 deletion(-) rename app/src/main/java/org/mozilla/fenix/components/metrics/{Metrics.kt => Event.kt} (99%) diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt similarity index 99% rename from app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt rename to app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt index c22cec86b..1768829c4 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt @@ -505,4 +505,3 @@ sealed class Event { internal open val extras: Map<*, String>? get() = null } - From 5d8c900391a8f545edb6cfead62e1b12fbf8fa6f Mon Sep 17 00:00:00 2001 From: Michael Comella Date: Tue, 21 Jul 2020 15:28:02 -0700 Subject: [PATCH 61/74] For #12802: add StorageStats glean metrics. --- app/metrics.yaml | 79 +++++++++++++++++++ .../org/mozilla/fenix/FenixApplication.kt | 13 +++ .../mozilla/fenix/perf/StorageStatsMetrics.kt | 66 ++++++++++++++++ .../fenix/perf/StorageStatsMetricsTest.kt | 61 ++++++++++++++ docs/metrics.md | 4 + 5 files changed, 223 insertions(+) create mode 100644 app/src/main/java/org/mozilla/fenix/perf/StorageStatsMetrics.kt create mode 100644 app/src/test/java/org/mozilla/fenix/perf/StorageStatsMetricsTest.kt diff --git a/app/metrics.yaml b/app/metrics.yaml index 37a9de4f1..483e4e2c8 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -3236,3 +3236,82 @@ autoplay: notification_emails: - fenix-core@mozilla.com expires: "2021-02-01" + +storage.stats: + query_stats_duration: + send_in_pings: + - metrics + type: timing_distribution + description: > + How long it took to query the device for the StorageStats that contain the + file size information. The docs say it may be expensive so we want to + ensure it's not too expensive. This value is only available on Android + 8+. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/12802 + data_reviews: + - todo + notification_emails: + - fenix-core@mozilla.com + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2020-12-21" + app_bytes: + send_in_pings: + - metrics + type: memory_distribution + description: > + The size of the app's APK and related files as installed: this is expected + to be larger than download size. This is the output of + [StorageStats.getAppBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getAppBytes()) + so see that for details. This value is only available on Android 8+. A + similar value may be available on the Google Play dashboard: we can use + this value to see if that value is reliable enough. + memory_unit: byte + bugs: + - https://github.com/mozilla-mobile/fenix/issues/12802 + data_reviews: + - todo + notification_emails: + - fenix-core@mozilla.com + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2020-12-21" + cache_bytes: + send_in_pings: + - metrics + type: memory_distribution + description: > + The size of all cached data in the app. This is the output of + [StorageStats.getCacheBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getCacheBytes()) + so see that for details. This value is only available on Android 8+. + memory_unit: byte + bugs: + - https://github.com/mozilla-mobile/fenix/issues/12802 + data_reviews: + - todo + notification_emails: + - fenix-core@mozilla.com + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2020-12-21" + data_dir_bytes: + send_in_pings: + - metrics + type: memory_distribution + description: > + The size of all data minus `cache_bytes`. This is the output of + [StorageStats.getDataBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getDataBytes()) + except we subtract the value of `cache_bytes` so the cache is not measured + redundantly; see that method for details. This value is only available on + Android 8+. + memory_unit: byte + bugs: + - https://github.com/mozilla-mobile/fenix/issues/12802 + data_reviews: + - todo + notification_emails: + - fenix-core@mozilla.com + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2020-12-21" diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index 201ba43d1..e9ea55bbc 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -44,6 +44,7 @@ import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.metrics.MetricServiceType import org.mozilla.fenix.ext.resetPoliciesAfter import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.perf.StorageStatsMetrics import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.push.PushFxaIntegration import org.mozilla.fenix.push.WebPushEngineIntegration @@ -205,12 +206,24 @@ open class FenixApplication : LocaleAwareApplication(), Provider { } } + fun queueMetrics() { + if (SDK_INT >= Build.VERSION_CODES.O) { // required by StorageStatsMetrics. + taskQueue.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. + StorageStatsMetrics.report(this.applicationContext) + } + } + } + initQueue() // We init these items in the visual completeness queue to avoid them initing in the critical // startup path, before the UI finishes drawing (i.e. visual completeness). queueInitExperiments() queueInitStorageAndServices() + queueMetrics() } private fun startMetricsIfEnabled() { diff --git a/app/src/main/java/org/mozilla/fenix/perf/StorageStatsMetrics.kt b/app/src/main/java/org/mozilla/fenix/perf/StorageStatsMetrics.kt new file mode 100644 index 000000000..af1463612 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/StorageStatsMetrics.kt @@ -0,0 +1,66 @@ +/* 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.app.usage.StorageStats +import android.app.usage.StorageStatsManager +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.PRIVATE +import androidx.annotation.WorkerThread +import androidx.core.content.getSystemService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.mozilla.fenix.GleanMetrics.StorageStats as Metrics + +/** + * A collection of functions related to measuring the [StorageStats] of the application such as data + * dir size. + * + * Unfortunately, this API is only available on API 26+ so the data will only be reported for those + * platforms. + */ +@RequiresApi(Build.VERSION_CODES.O) // StorageStatsManager +object StorageStatsMetrics { + + fun report(context: Context) { + GlobalScope.launch(Dispatchers.IO) { + reportSync(context) + } + } + + // I couldn't get runBlockingTest to work correctly so I moved the functionality under test to + // a synchronous function. + @VisibleForTesting(otherwise = PRIVATE) + @WorkerThread // queryStatsForUid + fun reportSync(context: Context) { + // I don't expect this to ever be null so we don't report if so. + context.getSystemService()?.let { storageStatsManager -> + val appInfo = context.applicationInfo + val storageStats = Metrics.queryStatsDuration.measure { + // The docs say queryStatsForPackage may be slower if the app uses + // android:sharedUserId so we the suggested alternative. + // + // The docs say this may be slow: + // > This method may take several seconds to complete, so it should only be called + // > from a worker thread. + // + // So we call from a worker thread and measure the duration to make sure it's not + // too slow. + storageStatsManager.queryStatsForUid(appInfo.storageUuid, appInfo.uid) + } + + // dataBytes includes the cache so we subtract it. + val justDataDirBytes = storageStats.dataBytes - storageStats.cacheBytes + + Metrics.dataDirBytes.accumulate(justDataDirBytes) + Metrics.appBytes.accumulate(storageStats.appBytes) + Metrics.cacheBytes.accumulate(storageStats.cacheBytes) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/perf/StorageStatsMetricsTest.kt b/app/src/test/java/org/mozilla/fenix/perf/StorageStatsMetricsTest.kt new file mode 100644 index 000000000..e66a69fb8 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/perf/StorageStatsMetricsTest.kt @@ -0,0 +1,61 @@ +/* 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.app.usage.StorageStats +import android.app.usage.StorageStatsManager +import android.content.Context +import androidx.core.content.getSystemService +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.RelaxedMockK +import mozilla.components.service.glean.testing.GleanTestRule +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.GleanMetrics.StorageStats as Metrics + +@RunWith(FenixRobolectricTestRunner::class) // gleanTestRule +class StorageStatsMetricsTest { + + @get:Rule + val gleanTestRule = GleanTestRule(testContext) + + @RelaxedMockK private lateinit var mockContext: Context + @RelaxedMockK private lateinit var storageStats: StorageStats + + @Before + fun setUp() { + MockKAnnotations.init(this) + + every { + mockContext.getSystemService()?.queryStatsForUid(any(), any()) + } returns storageStats + } + + @Test + fun `WHEN reporting THEN the values from the storageStats are accumulated`() { + every { storageStats.appBytes } returns 100 + every { storageStats.cacheBytes } returns 200 + every { storageStats.dataBytes } returns 1000 + + StorageStatsMetrics.reportSync(mockContext) + + assertEquals(100, Metrics.appBytes.testGetValue().sum) + assertEquals(200, Metrics.cacheBytes.testGetValue().sum) + assertEquals(800, Metrics.dataDirBytes.testGetValue().sum) + } + + @Test + fun `WHEN reporting THEN the query duration is measured`() { + StorageStatsMetrics.reportSync(mockContext) + assertTrue(Metrics.queryStatsDuration.testHasValue()) + } +} diff --git a/docs/metrics.md b/docs/metrics.md index c9482a603..0d85cf5b0 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -311,6 +311,10 @@ The following metrics are added to the ping: | search.default_engine.code |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |If the search engine is pre-loaded with Fenix this value will be the search engine identifier. If it's a custom search engine (defined: https://github.com/mozilla-mobile/fenix/issues/1607) the value will be "custom" |[1](https://github.com/mozilla-mobile/fenix/pull/1606), [2](https://github.com/mozilla-mobile/fenix/pull/5216)||2020-10-01 | | | search.default_engine.name |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |If the search engine is pre-loaded with Fenix this value will be the search engine name. If it's a custom search engine (defined: https://github.com/mozilla-mobile/fenix/issues/1607) the value will be "custom" |[1](https://github.com/mozilla-mobile/fenix/pull/1606), [2](https://github.com/mozilla-mobile/fenix/pull/5216)||2020-10-01 | | | search.default_engine.submission_url |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |If the search engine is pre-loaded with Fenix this value will be he base URL we use to build the search query for the search engine. For example: https://mysearchengine.com/?query=%s. If it's a custom search engine (defined: https://github.com/mozilla-mobile/fenix/issues/1607) the value will be "custom" |[1](https://github.com/mozilla-mobile/fenix/pull/1606), [2](https://github.com/mozilla-mobile/fenix/pull/5216)||2020-10-01 | | +| storage.stats.app_bytes |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |The size of the app's APK and related files as installed: this is expected to be larger than download size. This is the output of [StorageStats.getAppBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getAppBytes()) so see that for details. This value is only available on Android 8+. A similar value may be available on the Google Play dashboard: we can use this value to see if that value is reliable enough. |[1](todo)||2020-12-21 | | +| storage.stats.cache_bytes |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |The size of all cached data in the app. This is the output of [StorageStats.getCacheBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getCacheBytes()) so see that for details. This value is only available on Android 8+. |[1](todo)||2020-12-21 | | +| storage.stats.data_dir_bytes |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |The size of all data minus `cache_bytes`. This is the output of [StorageStats.getDataBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getDataBytes()) except we subtract the value of `cache_bytes` so the cache is not measured redundantly; see that method for details. This value is only available on Android 8+. |[1](todo)||2020-12-21 | | +| storage.stats.query_stats_duration |[timing_distribution](https://mozilla.github.io/glean/book/user/metrics/timing_distribution.html) |How long it took to query the device for the StorageStats that contain the file size information. The docs say it may be expensive so we want to ensure it's not too expensive. This value is only available on Android 8+. |[1](todo)||2020-12-21 | | ## startup-timeline From 849e5b3a092f517dedf2409a7bf17bad96d756aa Mon Sep 17 00:00:00 2001 From: Michael Comella Date: Fri, 31 Jul 2020 15:14:54 -0700 Subject: [PATCH 62/74] For #12802 - review: update metrics for data review. --- app/metrics.yaml | 16 ++++++++-------- docs/metrics.md | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/metrics.yaml b/app/metrics.yaml index 483e4e2c8..3b78f720c 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -3250,12 +3250,12 @@ storage.stats: bugs: - https://github.com/mozilla-mobile/fenix/issues/12802 data_reviews: - - todo + - https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732 notification_emails: - fenix-core@mozilla.com - perf-android-fe@mozilla.com - mcomella@mozilla.com - expires: "2020-12-21" + expires: "2021-02-01" app_bytes: send_in_pings: - metrics @@ -3271,12 +3271,12 @@ storage.stats: bugs: - https://github.com/mozilla-mobile/fenix/issues/12802 data_reviews: - - todo + - https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732 notification_emails: - fenix-core@mozilla.com - perf-android-fe@mozilla.com - mcomella@mozilla.com - expires: "2020-12-21" + expires: "2021-02-01" cache_bytes: send_in_pings: - metrics @@ -3289,12 +3289,12 @@ storage.stats: bugs: - https://github.com/mozilla-mobile/fenix/issues/12802 data_reviews: - - todo + - https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732 notification_emails: - fenix-core@mozilla.com - perf-android-fe@mozilla.com - mcomella@mozilla.com - expires: "2020-12-21" + expires: "2021-02-01" data_dir_bytes: send_in_pings: - metrics @@ -3309,9 +3309,9 @@ storage.stats: bugs: - https://github.com/mozilla-mobile/fenix/issues/12802 data_reviews: - - todo + - https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732 notification_emails: - fenix-core@mozilla.com - perf-android-fe@mozilla.com - mcomella@mozilla.com - expires: "2020-12-21" + expires: "2021-02-01" diff --git a/docs/metrics.md b/docs/metrics.md index 0d85cf5b0..b21181614 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -311,10 +311,10 @@ The following metrics are added to the ping: | search.default_engine.code |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |If the search engine is pre-loaded with Fenix this value will be the search engine identifier. If it's a custom search engine (defined: https://github.com/mozilla-mobile/fenix/issues/1607) the value will be "custom" |[1](https://github.com/mozilla-mobile/fenix/pull/1606), [2](https://github.com/mozilla-mobile/fenix/pull/5216)||2020-10-01 | | | search.default_engine.name |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |If the search engine is pre-loaded with Fenix this value will be the search engine name. If it's a custom search engine (defined: https://github.com/mozilla-mobile/fenix/issues/1607) the value will be "custom" |[1](https://github.com/mozilla-mobile/fenix/pull/1606), [2](https://github.com/mozilla-mobile/fenix/pull/5216)||2020-10-01 | | | search.default_engine.submission_url |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |If the search engine is pre-loaded with Fenix this value will be he base URL we use to build the search query for the search engine. For example: https://mysearchengine.com/?query=%s. If it's a custom search engine (defined: https://github.com/mozilla-mobile/fenix/issues/1607) the value will be "custom" |[1](https://github.com/mozilla-mobile/fenix/pull/1606), [2](https://github.com/mozilla-mobile/fenix/pull/5216)||2020-10-01 | | -| storage.stats.app_bytes |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |The size of the app's APK and related files as installed: this is expected to be larger than download size. This is the output of [StorageStats.getAppBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getAppBytes()) so see that for details. This value is only available on Android 8+. A similar value may be available on the Google Play dashboard: we can use this value to see if that value is reliable enough. |[1](todo)||2020-12-21 | | -| storage.stats.cache_bytes |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |The size of all cached data in the app. This is the output of [StorageStats.getCacheBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getCacheBytes()) so see that for details. This value is only available on Android 8+. |[1](todo)||2020-12-21 | | -| storage.stats.data_dir_bytes |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |The size of all data minus `cache_bytes`. This is the output of [StorageStats.getDataBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getDataBytes()) except we subtract the value of `cache_bytes` so the cache is not measured redundantly; see that method for details. This value is only available on Android 8+. |[1](todo)||2020-12-21 | | -| storage.stats.query_stats_duration |[timing_distribution](https://mozilla.github.io/glean/book/user/metrics/timing_distribution.html) |How long it took to query the device for the StorageStats that contain the file size information. The docs say it may be expensive so we want to ensure it's not too expensive. This value is only available on Android 8+. |[1](todo)||2020-12-21 | | +| storage.stats.app_bytes |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |The size of the app's APK and related files as installed: this is expected to be larger than download size. This is the output of [StorageStats.getAppBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getAppBytes()) so see that for details. This value is only available on Android 8+. A similar value may be available on the Google Play dashboard: we can use this value to see if that value is reliable enough. |[1](https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732)||2021-02-01 | | +| storage.stats.cache_bytes |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |The size of all cached data in the app. This is the output of [StorageStats.getCacheBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getCacheBytes()) so see that for details. This value is only available on Android 8+. |[1](https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732)||2021-02-01 | | +| storage.stats.data_dir_bytes |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |The size of all data minus `cache_bytes`. This is the output of [StorageStats.getDataBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getDataBytes()) except we subtract the value of `cache_bytes` so the cache is not measured redundantly; see that method for details. This value is only available on Android 8+. |[1](https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732)||2021-02-01 | | +| storage.stats.query_stats_duration |[timing_distribution](https://mozilla.github.io/glean/book/user/metrics/timing_distribution.html) |How long it took to query the device for the StorageStats that contain the file size information. The docs say it may be expensive so we want to ensure it's not too expensive. This value is only available on Android 8+. |[1](https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732)||2021-02-01 | | ## startup-timeline From 76d49fc050b2b1bee1f8064b9b6378c1bf8d784d Mon Sep 17 00:00:00 2001 From: mozilla-l10n-automation-bot <54512241+mozilla-l10n-automation-bot@users.noreply.github.com> Date: Fri, 31 Jul 2020 20:31:52 -0400 Subject: [PATCH 63/74] Import l10n. (#13189) --- app/src/main/res/values-zh-rCN/strings.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index ca47a107c..3615f0b5a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -39,7 +39,7 @@ 退出多选模式 - 将选择的的标签页保存到收藏集 + 将选中的的标签页保存到收藏集 已选择 %1$s @@ -54,7 +54,7 @@ 已选择 - %1$s —— Mozilla 荣誉出品 + %1$s 由 Mozilla 倾力打造。 @@ -77,11 +77,11 @@ - 添加小工具到主屏幕,让您更快开启 Firefox。 + 在主屏幕添加微件,一键启动 Firefox。 - 添加小工具 + 添加微件 - 暂时不要 + 现在不要 @@ -171,7 +171,7 @@ 搜索 - 依照设备语言显示 (ISO 3166 / 639) + 依照设备语言显示 (ISO 3166/639) 搜索语言 @@ -320,7 +320,7 @@ 立即同步 - 选择要同步的信息 + 选择要同步的项目 历史记录 @@ -751,7 +751,7 @@ 收藏集菜单 - 收藏对您而言重要的东西 + 有用的东西收藏在这 将相似的搜索、网站和标签页分组归并,方便以后快速访问。 From a8a0215b637d80a05fec42c9eaddcad8e272126d Mon Sep 17 00:00:00 2001 From: MickeyMoz Date: Sat, 1 Aug 2020 14:11:02 +0000 Subject: [PATCH 64/74] Update Android Components version to 53.0.20200801130512. --- buildSrc/src/main/java/AndroidComponents.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/AndroidComponents.kt b/buildSrc/src/main/java/AndroidComponents.kt index 0af20d7cb..f3b61a9a4 100644 --- a/buildSrc/src/main/java/AndroidComponents.kt +++ b/buildSrc/src/main/java/AndroidComponents.kt @@ -3,5 +3,5 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ object AndroidComponents { - const val VERSION = "53.0.20200731130051" + const val VERSION = "53.0.20200801130512" } From 65793f7fd75813e2cd96026dab94daccd51fb717 Mon Sep 17 00:00:00 2001 From: Mozilla L10n Automation Bot Date: Sun, 2 Aug 2020 00:07:16 +0000 Subject: [PATCH 65/74] Import l10n. --- app/src/main/res/values-bs/strings.xml | 62 ++++- app/src/main/res/values-ca/strings.xml | 65 ++++- app/src/main/res/values-da/strings.xml | 76 ++++-- app/src/main/res/values-el/strings.xml | 35 ++- app/src/main/res/values-ja/strings.xml | 45 +++- app/src/main/res/values-pa-rIN/strings.xml | 64 ++++- app/src/main/res/values-pl/strings.xml | 27 ++ app/src/main/res/values-ru/strings.xml | 29 ++- app/src/main/res/values-uz/strings.xml | 273 +++++++++++++++++++++ 9 files changed, 617 insertions(+), 59 deletions(-) diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index b5ab28881..bf619e6ae 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -22,6 +22,28 @@ %1$s otvoren tab. Dodirnite za promjenu tabova. + + %1$d označeno + + Dodaj novu kolekciju + + Naziv + + Izaberite kolekciju + + Izađite iz režima s više izbora + + Spasi označene tabove u kolekciju + + Označeno %1$s + + + Izašao iz režima s više izbora + + Ušao u režim s više izbora, označite tabove da ih spasite u kolekciju + + Označeno + %1$s je razvila Mozilla. @@ -143,13 +165,11 @@ Skeniraj - Prečice + Pretraživač Postavke pretraživača - - Traži na - Ovaj put, traži na: + Ovaj put, traži sa: Popuni link iz clipboarda @@ -256,8 +276,8 @@ Razvojni alati Udaljeno debagiranje preko USB-a - - Prikaži prečice za pretraživanje + + Prikaži pretraživače Prikaži prijedloge za pretraživanje @@ -502,6 +522,9 @@ %1$s (Privatni režim) + + Spasi + Obriši historiju @@ -567,6 +590,8 @@ Izaberite direktorij Da li ste sigurni da želite obrisati ovaj direktorij? + + %s će obrisati označene stavke. Obrisao %1$s @@ -621,8 +646,10 @@ Obrisao %1$s - + Zabilješke obrisane + + Brišem označene direktorije VRATI @@ -716,6 +743,8 @@ %d tab označen Tabovi spašeni! + + Kolekcija spašena! Tab spašen! @@ -819,6 +848,10 @@ ODBIJ Da li ste sigurni da želite obrisati %1$s? + + Brisanje ovog taba će obrisati cjelokupnu kolekciju. Nove kolekcije možete kreirati u bilo kojem trenutku. + + Obrisati %1$s? Obriši @@ -1216,6 +1249,8 @@ Prijave i lozinke koje nisu spašene će biti prikazane ovdje. Prijave i lozinke neće biti spašene za ove web stranice. + + Obriši sve izuzetke Pretraži prijave @@ -1255,6 +1290,8 @@ Kopiraj korisničko ime Kopiraj stranicu + + Otvori stranicu u browseru Prikaži lozinku @@ -1428,4 +1465,13 @@ OK, razumijem - + + + Prečice + + Traži na + + Ovaj put, traži na: + + Prikaži prečice za pretraživanje + diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 97427024b..41a63bfa2 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -26,6 +26,29 @@ %1$s pestanyes obertes. Toqueu per canviar de pestanya. + + %1$d seleccionades + + Afegeix una col·lecció nova + + Nom + + Trieu una col·lecció + + Surt del mode de selecció múltiple + + Desa les pestanyes seleccionades a la col·lecció + + S’ha seleccionat %1$s + + S’ha desseleccionat %1$s + + S’ha sortit del mode de selecció múltiple + + S’ha entrat en el mode de selecció múltiple, seleccioneu pestanyes per desar-les en una col·lecció + + S’ha seleccionat + El %1$s està creat per Mozilla. @@ -148,13 +171,11 @@ Escaneja - Dreceres + Motor de cerca Paràmetres del motor de cerca - - Cerca amb - Aquesta vegada, cerca amb: + Aquesta vegada, cerca amb: Utilitza l’enllaç del porta-retalls @@ -265,8 +286,8 @@ Eines per a desenvolupadors Depuració remota per USB - - Mostra dreceres de cerca + + Mostra els motors de cerca Mostra suggeriments de cerca @@ -513,6 +534,9 @@ %1$s (mode privat) + + Desa + Suprimeix l’historial @@ -579,6 +603,8 @@ Trieu una carpeta Segur que voleu suprimir aquesta carpeta? + + El %s suprimirà els elements seleccionats. S’ha suprimit %1$s @@ -633,8 +659,10 @@ S’ha suprimit %1$s - + S’han suprimit les adreces d’interès + + S’estan suprimint les carpetes seleccionades DESFÉS @@ -728,6 +756,8 @@ %d pestanya seleccionada S’han desat les pestanyes + + S’ha desat la col·lecció. S’ha desat la pestanya @@ -833,6 +863,10 @@ DENEGA Segur que voleu suprimir %1$s? + + Si suprimiu aquesta pestanya, suprimireu tota la col·lecció. Podeu crear col·leccions noves en qualsevol moment. + + Voleu suprimir %1$s? Suprimeix @@ -890,8 +924,6 @@ Suprimeix automàticament les dades de navegació en seleccionar «Surt» en el menú principal - - Historial de navegació Surt @@ -1236,6 +1268,8 @@ Els inicis de sessió i les contrasenyes que no es desin es mostraran aquí. No es desaran els inicis de sessió ni les contrasenyes per a aquests llocs. + + Suprimeix totes les excepcions Cerca els inicis de sessió @@ -1274,6 +1308,8 @@ Copia el nom d’usuari Copia el lloc + + Obre el lloc en el navegador Mostra la contrasenya @@ -1450,4 +1486,13 @@ Entesos - + + + Dreceres + + Cerca amb + + Aquesta vegada, cerca amb: + + Mostra dreceres de cerca + diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index c21238321..72cc9afc6 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -24,6 +24,31 @@ %1$s åbne faneblade. Tryk for at skifte faneblade. + + %1$d valgt + + + Tilføj ny samling + + Navn + + Vælg samling + + + Afslut flervalgs-tilstand + + Gem valgte faneblade til samling + + %1$s valgt + + %1$s ikke valgt + + Afsluttede flervalgs-tilstand + + Åbnede flervalgs-tilstand - vælg faneblade at gemme til en samling + + Valgt + %1$s er lavet af Mozilla. @@ -32,8 +57,7 @@ Du befinder dig i en privat session - ¶ - · %1$s rydder din søge- og browserhistorik fra private faneblade, når du lukker dem, eller når du afslutter programmet. Det gør det nemmere at holde din færden på nettet for dig selv, hvis andre bruger den samme computer. Websteder og din internetudbyder kan dog stadig finde ud af, hvad du foretager dig. + %1$s rydder din søge- og browserhistorik fra private faneblade, når du lukker dem, eller når du afslutter programmet. Det gør det nemmere at holde din færden på nettet for dig selv, hvis andre bruger den samme computer. Websteder og din internetudbyder kan dog stadig finde ud af, hvad du foretager dig. Almindelige myter om privat browsing Slet session @@ -146,13 +170,11 @@ Skan - Genveje + Søgetjeneste Indstillinger for søgetjenester - - Søg med - Søg denne gang med: + Søg denne gang med: Udfyld link fra udklipsholderen @@ -187,7 +209,7 @@ Standard-søgetjeneste - Søg + Søgning Adressefelt @@ -258,9 +280,9 @@ Udviklerværktøj - Fjern-debugging vis USB - - Vis søge-genveje + Fjern-debugging via USB + + Vis søgetjenester Vis søgeforslag @@ -505,6 +527,9 @@ %1$s (Privat tilstand) + + Gem + Slet historik @@ -570,6 +595,8 @@ Vælg mappe Er du sikker på, at du vil slette denne mappe? + + %s vil slette de valgte elementer. %1$s slettet @@ -624,8 +651,10 @@ %1$s blev slettet - + Bogmærker slettet + + Sletter valgte mapper FORTRYD @@ -718,6 +747,8 @@ %d faneblad valgt Faneblade gemt! + + Samling gemt! Faneblad gemt! @@ -822,6 +853,10 @@ AFVIS Er du sikker på, at du vil slette %1$s? + + Hele samlingen bliver slettet, hvis du sletter dette faneblad. Du kan oprette nye samlinger når som helst. + + Slet %1$s? Slet @@ -1215,9 +1250,11 @@ Undtagelser - Login og adgangskoder, der ikke er gemt, vises her. + Logins og adgangskoder, der ikke er gemt, vises her. Logins og adgangskoder vil ikke blive gemt for disse websteder. + + Slet alle undtagelser Søg efter logins @@ -1256,6 +1293,8 @@ Kopier brugernavn Kopier websted + + Åbn websted i browser Vis adgangskode @@ -1305,7 +1344,7 @@ Søgestreng der skal anvendes - Erstat forespørgslen med “%s”, Eksempel: \n https://www.google.com/search?q= %s + Erstat forespørgslen med “%s”, Eksempel: \n https://www.google.com/search?q=%s Læs mere @@ -1429,4 +1468,13 @@ Ok, forstået - + + + Genveje + + Søg med + + Søg denne gang med: + + Vis søge-genveje + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 8972f0947..4410c1bd1 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -27,6 +27,13 @@ %1$s ανοικτές καρτέλες. Πατήστε για εναλλαγή καρτελών. + + Προσθήκη νέας συλλογής + + Όνομα + + Επιλογή συλλογής + Το %1$s αναπτύσσεται από τη Mozilla. @@ -141,13 +148,11 @@ Σάρωση - Συντομεύσεις + Μηχανή αναζήτησης Ρυθμίσεις μηχανής αναζήτησης - - Αναζήτηση με - Αυτή τη φορά, αναζήτηση με: + Αυτή τη φορά, αναζήτηση με: Συμπλήρωση συνδέσμου από το πρόχειρο @@ -250,8 +255,6 @@ Εργαλεία προγραμματιστή Απομακρυσμένος εντοπισμός σφαλμάτων μέσω USB - - Εμφάνιση συντομεύσεων αναζήτησης Εμφάνιση προτάσεων αναζήτησης @@ -469,6 +472,9 @@ %1$s (Ιδιωτική λειτουργία) + + Αποθήκευση + Διαγραφή ιστορικού @@ -587,7 +593,7 @@ Άκυρο URL Κανένας σελιδοδείκτης εδώ - + Οι σελιδοδείκτες διαγράφηκαν ΑΝΑΙΡΕΣΗ @@ -771,6 +777,8 @@ ΑΡΝΗΣΗ Θέλετε σίγουρα να διαγράψετε το %1$s; + + Διαγραφή του %1$s; Διαγραφή @@ -1057,6 +1065,8 @@ Μάθετε περισσότερα σχετικά με το Sync. Εξαιρέσεις + + Διαγραφή όλων των εξαιρέσεων Αναζήτηση συνδέσεων @@ -1218,4 +1228,13 @@ OK, το κατάλαβα - + + + Συντομεύσεις + + Αναζήτηση με + + Αυτή τη φορά, αναζήτηση με: + + Εμφάνιση συντομεύσεων αναζήτησης + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 2ba635e9c..584f965e6 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -27,6 +27,21 @@ 開いているタブ %1$s 個。タップしてタブを切り替えます。 + + %1$d 個選択 + + 新しいコレクションを追加 + + コレクション名 + + コレクションを選択 + + 複数選択モードを終了 + + 選択したタブをコレクションに保存 + + 複数選択モードを終了しました + %1$s は Mozilla の製品です。 @@ -152,13 +167,11 @@ スキャン - ショートカット + 検索エンジン 検索エンジンの設定 - - 検索: - 今回だけ使う検索エンジン: + 今回だけ使う検索エンジン: クリップボードからリンクを入力 @@ -268,8 +281,8 @@ 開発者ツール USB 経由でリモートデバッグする - - 検索ショートカットを表示 + + 検索エンジンを表示 検索語句の候補を表示 @@ -522,6 +535,9 @@ %1$s (プライベートモード) + + 保存 + 履歴を削除 @@ -645,7 +661,7 @@ %1$s を削除しました - + ブックマークを削除しました 元に戻す @@ -906,8 +922,6 @@ メインメニューから [終了] を選択すると、ブラウジングデータが自動的に削除されます メインメニューから [終了] を選択すると、ブラウジングデータが自動的に削除されます - - ブラウジング履歴 終了 @@ -1288,6 +1302,8 @@ ユーザー名をコピー サイトをコピー + + サイトをブラウザーで開く パスワードを表示 @@ -1464,4 +1480,13 @@ OK - + + + ショートカット + + 検索: + + 今回だけ使う検索エンジン: + + 検索ショートカットを表示 + diff --git a/app/src/main/res/values-pa-rIN/strings.xml b/app/src/main/res/values-pa-rIN/strings.xml index a6f579f6d..980a4498c 100644 --- a/app/src/main/res/values-pa-rIN/strings.xml +++ b/app/src/main/res/values-pa-rIN/strings.xml @@ -25,6 +25,30 @@ %1$s ਟੈਬਾਂ ਖੁੱਲ੍ਹੀ। ਟੈਬਾਂ ਲਈ ਸਵਿੱਚ ਕਰਨ ਵਾਸਤੇ ਟੈਪ ਕਰੋ। + + %1$d ਚੁਣੀਆਂ + + ਨਵਾਂ ਭੰਡਾਰ ਜੋੜੋ + + ਨਾਂ + + + ਭੰਡਾਰ ਚੁਣੋ + + ਬਹੁ-ਚੋਣ ਢੰਗ ਵਿੱਚੋਂ ਬਾਹਰ ਜਾਓ + + ਚੁਣੀਆਂ ਟੈਬਾਂ ਨੂੰ ਭੰਡਾਰ ਵਿੱਚ ਸੰਭਾਲੋ + + %1$s ਚੁਣੀ + + %1$s ਨਹੀਂ ਚੁਣੀ + + ਬਹੁ-ਚੋਣ ਢੰਗ ਤੋਂ ਬਾਹਰ ਗਏ + + ਬਹੁ-ਚੋਣ ਢੰਗ ਵਿੱਚ ਹੋ, ਚੁਣੀਆਂ ਟੈਬਾਂ ਨੂੰ ਭੰਡਾਰ ਵਿੱਚ ਸੰਭਾਲਿਆ ਜਾਵੇਗਾ। + + ਚੁਣੇ ਹੋਏ + %1$s ਮੌਜ਼ੀਲਾ ਵਲੋਂ ਤਿਆਰ ਕੀਤਾ @@ -152,13 +176,11 @@ ਸਕੈਨ ਕਰੋ - ਸ਼ਾਰਟਕੱਟ + ਖੋਜ ਇੰਜਣ ਖੋਜ ਇੰਜਣ ਸੈਟਿੰਗਾਂ - - ਇਸ ਨਾਲ ਖੋਜੋ - ਇਸ ਵੇਲੇ, ਇਸ ਨਾਲ ਖੋਜੋ: + ਇਸ ਵੇਲੇ, ਇਸ ਨਾਲ ਖੋਜੋ: ਕਲਿੱਪਬੋਰਡ ਤੋਂ ਲਿੰਕ ਭਰੋ @@ -266,8 +288,8 @@ ਡਿਵੈਲਪਰ ਸੰਦ USB ਰਾਹੀਂ ਰਿਮੋਟ ਡੀਬੱਗ ਕਰਨਾ - - ਖੋਜ ਸ਼ਾਰਟਕੱਟ ਵੇਖਾਓ + + ਖੋਜ ਇੰਜਣ ਵੇਖੋ ਖੋਜ ਸੁਝਾਅ ਵੇਖਾਓ @@ -521,6 +543,9 @@ %1$s (ਪ੍ਰਾਈਵੇਟ ਮੋਡ) + + ਸੰਭਾਲੋ + ਅਤੀਤ ਹਟਾਓ @@ -587,6 +612,8 @@ ਫੋਲਡਰ ਚੁਣੋ ਕੀ ਤੁਸੀਂ ਇਹ ਫੋਲਡਰ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ? + + %s ਚੁਣੀਆਂ ਚੀਜ਼ਾਂ ਨੂੰ ਹਟਾਏਗਾ। %1$s ਨੂੰ ਹਟਾਇਆ @@ -641,8 +668,10 @@ %1$s ਹਟਾਇਆ - + ਬੁੱਕਮਾਰਕ ਹਟਾਇਆ + + ਚੁਣੇ ਫੋਲਡਰਾਂ ਨੂੰ ਹਟਾਇਆ ਜਾ ਰਿਹਾ ਹੈ UNDO @@ -736,6 +765,8 @@ %d ਟੈਬ ਚੁਣੀ ਟੈਬਾਂ ਸੰਭਾਲੀਆਂ! + + ਭੰਡਾਰ ਸੰਭਾਲਿਆ! ਟੈਬ ਸੰਭਾਲੀ! @@ -842,6 +873,10 @@ ਨਾਂਹ ਕਰੋ ਕੀ ਤੁਸੀਂ %1$s ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ? + + ਇਹ ਟੈਬ ਨੂੰ ਹਟਾਉਣ ਨਾਲ ਸਾਰਾ ਭੰਡਾਹ ਹਟਾਇਆ ਜਾਵੇਗਾ। ਤੁਸੀਂ ਕਿਸੇ ਵੀ ਵੇਲੇ ਨਵਾਂ ਭੰਡਾਰ ਬਣਾ ਸਕਦੇ ਹੋ। + + %1$s ਨੂੰ ਹਟਾਉਣਾ ਹੈ? ਹਟਾਓ @@ -1243,6 +1278,8 @@ ਨਾ ਸੰਭਾਲੇ ਹੋਏ ਲਾਗਇਨ ਅਤੇ ਪਾਸਵਰਡਾਂ ਨੂੰ ਇੱਥੇ ਸੰਭਾਲਿਆ ਜਾਵੇਗਾ। ਇਹਨਾਂ ਸਾਈਟਾਂ ਲਈ ਲਾਗਇਨ ਅਤੇ ਪਾਸਵਰਡ ਨਹੀਂ ਸੰਭਾਲੇ ਜਾਣਗੇ। + + ਸਾਰੀਆਂ ਛੋਟਾਂ ਹਟਾ ਦਿਓ ਲਾਗਇਨ ਖੋਜੋ @@ -1281,6 +1318,8 @@ ਵਰਤੋਂਕਾਰ-ਨਾਂ ਨੂੰ ਕਾਪੀ ਕਰੋ ਸਾਈਟ ਨੂੰ ਕਾਪੀ ਕਰੋ + + ਸਾਈਟ ਨੂੰ ਬਰਾਊਜ਼ਰ ਚ ਖੋਲ੍ਹੋ ਪਾਸਵਰਡ ਵੇਖਾਓ @@ -1455,4 +1494,13 @@ ਠੀਕ ਹੈ, ਸਮਝ ਗਏ - + + + ਸ਼ਾਰਟਕੱਟ + + ਇਸ ਨਾਲ ਖੋਜੋ + + ਇਸ ਵੇਲੇ, ਇਸ ਨਾਲ ਖੋਜੋ: + + ਖੋਜ ਸ਼ਾਰਟਕੱਟ ਵੇਖਾਓ + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 1d2a75161..e2a98b23a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -25,6 +25,30 @@ Otwarte karty: %1$s. Stuknij, aby przełączyć karty. + + Zaznaczone: %1$d + + Nowa kolekcja + + Nazwa + + Wybierz kolekcję + + Opuść tryb wielokrotnego wyboru + + Zachowaj zaznaczone karty w kolekcji + + Zaznaczono „%1$s” + + Odznaczono „%1$s” + + Opuszczono tryb wielokrotnego wyboru + + + Otwarto tryb wielokrotnego wyboru, zaznacz karty do zachowania w kolekcji + + Zaznaczone + %1$s jest tworzony przez Mozillę. @@ -513,6 +537,9 @@ %1$s (tryb prywatny) + + Zachowaj + Usuń historię diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0e2d38b00..44a9f4de7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -27,6 +27,30 @@ Открытых вкладок: %1$s. Нажмите, чтобы переключить вкладки. + + Выбрано: %1$d + + Создать новую коллекцию + + Название + + + Выберите коллекцию + + Выйти из режима множественного выбора + + Сохранить выбранные вкладки в коллекцию + + Выбрана %1$s + + Снят выбор с %1$s + + Режим множественного выбора отключен + + Включен режим множественного выбора, выберите вкладки для сохранения в коллекцию + + Выбрано + %1$s разработан Mozilla. @@ -519,6 +543,9 @@ %1$s (Приватный просмотр) + + Сохранить + Удалить историю @@ -1079,7 +1106,7 @@ Сканировать QR-код - Войти с распознаванием лица + Войти с помощью камеры Использовать электронную почту diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index 082743f14..b578a5515 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -23,6 +23,29 @@ %1$s ta ochiq varaq. Boshqa varaqqa oʻtish uchun bosing. + + Tanlandi: %1$d + + Yangi kolleksiya qoʻshildi + + Nomi + + Kolleksiyani tanlash + + Koʻp tanlov rejimidan chiqish + + Tanlangan varaqlarni kolleksiyaga saqlang + + %1$s tanlandi + + %1$s tanlovi bekor qilindi + + Koʻp tanlov rejimidan chiqdi + + Koʻp tanlov rejimiga kirdi. Kolleksiyaga saqlash uchun varaqlarni tanlang + + Tanlandi + %1$s brauzerini Mozilla ishlab chiqqan. @@ -143,6 +166,8 @@ Tekshirish + + Qidiruv tizimi Qidiruv tizimi sozlamalari @@ -317,6 +342,254 @@ Qabul qilingan varaqlar + + Boshqa Firefox qurilmalaridan olingan varaqlar uchun bildirishnomalar. + + Olingan varaq + + Olingan varaqlar + + + %s qurilmasidan varaq + + + + Kuzatuvdan himoya + + Kuzatuvdan himoya + + Sizni onlayn kuzatadigan kontent va skriptlarni bloklang + + Istisnolar + + Bu saytlar uchun kuzatuvdan himoya oʻchiq + + Barcha saytlar uchun yoqing + + Istisnolar tanlangan saytlar uchun kuzatuvdan himoyani oʻchirib qoʻyish imkonini beradi. + + Batafsil maʼlumot + + + Butun dunyo boʻyicha oʻchirilgan. Yoqish uchun Sozlamalarga kiring. + + + Telemetriya + + Texnik va foydalanishga oid maʼlumotlar + + + Maxfiy varaq qoʻshish + + Maxfiy + + Varaqlarni ochish + + Kolleksiyaga saqlash + + Barcha varaqlarni ulashish + + Ichki varaqlarni yopish + + Yangi varaq + + + Bosh sahifa qaytish + + Varaq rejimiga oʻtish + + Kolleksiyadan varaqni olib tashlash + + Varaqni yopish + + %s varagʻini yopish + + Varaqlar menyusini ochish + + Barcha varaqlarni yopish + + Varaqlarni ulashish + + Varaqlarni kolleksiyaga saqlash + + Varaqlar menyusi + + Varaqni ulashish + + Oʻchirish + + + Saqlash + + Ulashish + + Joriy seans tasviri + + Kolleksiyaga saqlash + + Kolleksiyani oʻchirish + + Kolleksiya nomini oʻzgartirish + + Varaqlarni ochish + + Olib tashlash + + %1$s (Maxfiy rejim) + + Saqlash + + + + Tarixni oʻchirish + + Haqiqatan ham brauzer tarixini tozalamoqchimisiz? + + Tarix oʻchirildi + + %1$s oʻchirildi + + Tozalash + + Nusxa olish + + Ulashish + + Yangi varaqda ochish + + Yangi maxfiy varaqda ochish + + Oʻchirish + + Tanlandi: %1$d + + %1$d ta elementni oʻchirish + + Soʻnggi 24 soat + + Soʻnggi 7 kun + + Soʻnggi 30 kun + + Eskiroq + + Tarix yoʻq + + + + Kechirasiz, %1$s bu sahifani yuklay olmadi. + + Pastdagi varaqni tiklash yoki yopish uchun urinib koʻrishingiz mumkin. + + Nosozlik hisobotni Mozillaga yuborish + + Varaqni yopish + + Yorliqni tiklash + + + Seans parametrlari + + + Seansni ulashish + + + + Xatchoʻp menyusi + + Xatchoʻpni tahrirlash + + Jildni tanlash + + Bu jildni oʻchirishni xohlaysizmi? + + %s tanlangan elementlarni oʻchiradi. + + %1$s oʻchirildi + + Jild qoʻshish + + Xatchoʻp yaratildi. + + + Xatchoʻp saqlandi + + TAHRIRLASH + + Tahrirlash + + Tanlash + + Nusxa olish + + Ulashish + + Yangi varaqda ochish + + Yangi maxfiy varaqda ochish + + Oʻchirish + + Saqlash + + Tanlandi: %1$d + + Xatchoʻpni tahrirlash + + Jildni tahrirlash + + Sinxronlangan xatchoʻplarni koʻrish uchun hisobingizga kiring + + URL + + JILD + + NOMI + + Jild qoʻshish + + Jildni tanlash + + Sarlavha boʻlishi kerak + + Xato URL + + Bu yerda hech qanday xatchoʻp yoʻq + + %1$s oʻchirildi + + Oʻchirilgan xatchoʻplar + + Tanlangan jildlarni oʻchirish + + BEKOR QILISH + + + + Ruxsatlar + + Sozlamalarga oʻting + + Tezkor sozlamalar paneli + + Tavsiya qilinadi + + Sayt ruxsatlarini boshqarish + + Ruxsatlarni tozalash + + Ruxsatni tozalash + + Barcha saytlardagi ruxsatlarni tozalash + + Avtomatik ravishda ishga tushirish + Koʻchirish tugadi From b52c4320f042461c13390403828a06881443b1c0 Mon Sep 17 00:00:00 2001 From: Shen Date: Thu, 30 Jul 2020 17:25:57 -0700 Subject: [PATCH 66/74] For #9619 hide overflow menu for all history items in selected mode --- .../history/viewholders/HistoryListItemViewHolder.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt index c14be966b..762f72aa9 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt @@ -68,10 +68,10 @@ class HistoryListItemViewHolder( itemView.history_layout.loadFavicon(item.url) } - if (item !in selectionHolder.selectedItems) { - itemView.overflow_menu.showAndEnable() - } else { + if (mode is HistoryFragmentState.Mode.Editing) { itemView.overflow_menu.hideAndDisable() + } else { + itemView.overflow_menu.showAndEnable() } this.item = item From bd41656453bb4cadcf257a1dacdf61f12626cca5 Mon Sep 17 00:00:00 2001 From: MickeyMoz Date: Sun, 2 Aug 2020 14:19:08 +0000 Subject: [PATCH 67/74] Update Android Components version to 53.0.20200802130440. --- buildSrc/src/main/java/AndroidComponents.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/AndroidComponents.kt b/buildSrc/src/main/java/AndroidComponents.kt index f3b61a9a4..c5dd2a027 100644 --- a/buildSrc/src/main/java/AndroidComponents.kt +++ b/buildSrc/src/main/java/AndroidComponents.kt @@ -3,5 +3,5 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ object AndroidComponents { - const val VERSION = "53.0.20200801130512" + const val VERSION = "53.0.20200802130440" } From ddfb3dfa722d576587db2374e29b702b3835adcf Mon Sep 17 00:00:00 2001 From: Ankur Khandelwal Date: Mon, 3 Aug 2020 02:53:39 +0530 Subject: [PATCH 68/74] Renamed Deps.mozilla_ui_publicsuffixlist to Deps.mozilla_lib_publicsuffixlist (#13201) --- app/build.gradle | 2 +- buildSrc/src/main/java/Dependencies.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0aab34e4e..6ad80e02a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -469,7 +469,7 @@ dependencies { implementation Deps.mozilla_ui_colors implementation Deps.mozilla_ui_icons - implementation Deps.mozilla_ui_publicsuffixlist + implementation Deps.mozilla_lib_publicsuffixlist implementation Deps.mozilla_ui_widgets implementation Deps.mozilla_lib_crash diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index b6540a220..876e537dd 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -138,7 +138,7 @@ object Deps { const val mozilla_lib_push_firebase = "org.mozilla.components:lib-push-firebase:${Versions.mozilla_android_components}" const val mozilla_lib_dataprotect = "org.mozilla.components:lib-dataprotect:${Versions.mozilla_android_components}" - const val mozilla_ui_publicsuffixlist = "org.mozilla.components:lib-publicsuffixlist:${Versions.mozilla_android_components}" + const val mozilla_lib_publicsuffixlist = "org.mozilla.components:lib-publicsuffixlist:${Versions.mozilla_android_components}" const val mozilla_support_base = "org.mozilla.components:support-base:${Versions.mozilla_android_components}" const val mozilla_support_images = "org.mozilla.components:support-images:${Versions.mozilla_android_components}" From f3f470a977a2146dc3d95ad7ebd6509941318f3e Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Sun, 2 Aug 2020 18:48:10 -0700 Subject: [PATCH 69/74] For #13140: Use concept-menu for saved logins menu (#13143) --- app/build.gradle | 1 + .../logins/SavedLoginsSortingStrategyMenu.kt | 77 +++++++++------- .../logins/fragment/SavedLoginsFragment.kt | 58 ++++-------- .../java/org/mozilla/fenix/utils/Settings.kt | 31 +++---- app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/styles.xml | 3 + .../SavedLoginsSortingStrategyMenuTest.kt | 88 +++++++++++++++++++ 7 files changed, 167 insertions(+), 92 deletions(-) create mode 100644 app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsSortingStrategyMenuTest.kt diff --git a/app/build.gradle b/app/build.gradle index 6ad80e02a..3f86afe6a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -410,6 +410,7 @@ dependencies { implementation Deps.mozilla_browser_domains implementation Deps.mozilla_browser_icons implementation Deps.mozilla_browser_menu + implementation Deps.mozilla_browser_menu2 implementation Deps.mozilla_browser_search implementation Deps.mozilla_browser_session implementation Deps.mozilla_browser_state diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsSortingStrategyMenu.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsSortingStrategyMenu.kt index 8ac54dc81..8e5fbe21d 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsSortingStrategyMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsSortingStrategyMenu.kt @@ -5,51 +5,68 @@ package org.mozilla.fenix.settings.logins import android.content.Context -import mozilla.components.browser.menu.BrowserMenuBuilder -import mozilla.components.browser.menu.item.SimpleBrowserMenuHighlightableItem +import androidx.annotation.VisibleForTesting +import mozilla.components.browser.menu2.BrowserMenuController +import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect +import mozilla.components.concept.menu.candidate.TextMenuCandidate +import mozilla.components.concept.menu.candidate.TextStyle import mozilla.components.support.ktx.android.content.getColorFromAttr import org.mozilla.fenix.R -import org.mozilla.fenix.theme.ThemeManager +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor class SavedLoginsSortingStrategyMenu( private val context: Context, - private val itemToHighlight: Item, - private val onItemTapped: (Item) -> Unit = {} + private val savedLoginsInteractor: SavedLoginsInteractor ) { - sealed class Item { - object AlphabeticallySort : Item() - object LastUsedSort : Item() + enum class Item(val strategyString: String) { + AlphabeticallySort("ALPHABETICALLY"), + LastUsedSort("LAST_USED"); + + companion object { + fun fromString(strategyString: String) = when (strategyString) { + AlphabeticallySort.strategyString -> AlphabeticallySort + LastUsedSort.strategyString -> LastUsedSort + else -> AlphabeticallySort + } + } } - val menuBuilder by lazy { BrowserMenuBuilder(menuItems) } + val menuController by lazy { BrowserMenuController() } - private val menuItems by lazy { - listOfNotNull( - SimpleBrowserMenuHighlightableItem( - label = context.getString(R.string.saved_logins_sort_strategy_alphabetically), - textColorResource = ThemeManager.resolveAttribute(R.attr.primaryText, context), - itemType = Item.AlphabeticallySort, - backgroundTint = context.getColorFromAttr(R.attr.colorControlHighlight), - isHighlighted = { itemToHighlight == Item.AlphabeticallySort } + @VisibleForTesting + internal fun menuItems(itemToHighlight: Item): List { + val textStyle = TextStyle( + color = context.getColorFromAttr(R.attr.primaryText) + ) + + val highlight = HighPriorityHighlightEffect( + backgroundTint = context.getColorFromAttr(R.attr.colorControlHighlight) + ) + + return listOf( + TextMenuCandidate( + text = context.getString(R.string.saved_logins_sort_strategy_alphabetically), + textStyle = textStyle, + effect = if (itemToHighlight == Item.AlphabeticallySort) highlight else null ) { - onItemTapped.invoke(Item.AlphabeticallySort) + savedLoginsInteractor.onSortingStrategyChanged( + SortingStrategy.Alphabetically(context.components.publicSuffixList) + ) }, - - SimpleBrowserMenuHighlightableItem( - label = context.getString(R.string.saved_logins_sort_strategy_last_used), - textColorResource = ThemeManager.resolveAttribute(R.attr.primaryText, context), - itemType = Item.LastUsedSort, - backgroundTint = context.getColorFromAttr(R.attr.colorControlHighlight), - isHighlighted = { itemToHighlight == Item.LastUsedSort } + TextMenuCandidate( + text = context.getString(R.string.saved_logins_sort_strategy_last_used), + textStyle = textStyle, + effect = if (itemToHighlight == Item.LastUsedSort) highlight else null ) { - onItemTapped.invoke(Item.LastUsedSort) + savedLoginsInteractor.onSortingStrategyChanged( + SortingStrategy.LastUsed + ) } ) } - internal fun updateMenu(itemToHighlight: Item) { - menuItems.forEach { - it.isHighlighted = { itemToHighlight == it.itemType } - } + fun updateMenu(itemToHighlight: Item) { + menuController.submitList(menuItems(itemToHighlight)) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt index 3975bb0fe..fe2431ae0 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt @@ -22,8 +22,8 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import kotlinx.android.synthetic.main.fragment_saved_logins.view.* import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.concept.menu.MenuController +import mozilla.components.concept.menu.Orientation import mozilla.components.lib.state.ext.consumeFrom import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity @@ -31,7 +31,6 @@ import org.mozilla.fenix.R import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.redirectToReAuth -import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.settings.logins.LoginsAction @@ -51,7 +50,6 @@ class SavedLoginsFragment : Fragment() { private lateinit var savedLoginsInteractor: SavedLoginsInteractor private lateinit var dropDownMenuAnchorView: View private lateinit var sortingStrategyMenu: SavedLoginsSortingStrategyMenu - private lateinit var sortingStrategyPopupMenu: BrowserMenu private lateinit var toolbarChildContainer: FrameLayout private lateinit var sortLoginsMenuRoot: ConstraintLayout private lateinit var loginsListController: LoginsListController @@ -121,10 +119,8 @@ class SavedLoginsFragment : Fragment() { return view } - @ObsoleteCoroutinesApi @ExperimentalCoroutinesApi override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) consumeFrom(savedLoginsStore) { sortingStrategyMenu.updateMenu(savedLoginsStore.state.highlightedItem) savedLoginsListView.update(it) @@ -161,7 +157,7 @@ class SavedLoginsFragment : Fragment() { toolbarChildContainer.removeAllViews() toolbarChildContainer.visibility = View.GONE (activity as HomeActivity).getSupportActionBarAndInflateIfNecessary().setDisplayShowTitleEnabled(true) - sortingStrategyPopupMenu.dismiss() + sortingStrategyMenu.menuController.dismiss() redirectToReAuth(listOf(R.id.loginDetailFragment), findNavController().currentDestination?.id) super.onPause() @@ -206,47 +202,27 @@ class SavedLoginsFragment : Fragment() { } private fun attachMenu() { - sortingStrategyPopupMenu = sortingStrategyMenu.menuBuilder.build(requireContext()) - - sortLoginsMenuRoot.setOnClickListener { - sortLoginsMenuRoot.isActivated = true - sortingStrategyPopupMenu.show( - anchor = dropDownMenuAnchorView, - orientation = BrowserMenu.Orientation.DOWN - ) { + sortingStrategyMenu.menuController.register(object : MenuController.Observer { + override fun onDismiss() { + // Deactivate button on dismiss sortLoginsMenuRoot.isActivated = false } + }, view = sortLoginsMenuRoot) + + sortLoginsMenuRoot.setOnClickListener { + // Activate button on show + sortLoginsMenuRoot.isActivated = true + sortingStrategyMenu.menuController.show( + anchor = dropDownMenuAnchorView, + orientation = Orientation.DOWN + ) } } private fun setupMenu(itemToHighlight: SavedLoginsSortingStrategyMenu.Item) { - sortingStrategyMenu = - SavedLoginsSortingStrategyMenu( - requireContext(), - itemToHighlight - ) { - when (it) { - SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort -> { - savedLoginsInteractor.onSortingStrategyChanged( - SortingStrategy.Alphabetically( - requireComponents.publicSuffixList - ) - ) - } - - SavedLoginsSortingStrategyMenu.Item.LastUsedSort -> { - savedLoginsInteractor.onSortingStrategyChanged( - SortingStrategy.LastUsed - ) - } - } - } + sortingStrategyMenu = SavedLoginsSortingStrategyMenu(requireContext(), savedLoginsInteractor) + sortingStrategyMenu.updateMenu(itemToHighlight) attachMenu() } - - companion object { - const val SORTING_STRATEGY_ALPHABETICALLY = "ALPHABETICALLY" - const val SORTING_STRATEGY_LAST_USED = "LAST_USED" - } } 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 cac21f0b5..910a73035 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -36,7 +36,6 @@ import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.deletebrowsingdata.DeleteBrowsingDataOnQuitType import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu import org.mozilla.fenix.settings.logins.SortingStrategy -import org.mozilla.fenix.settings.logins.fragment.SavedLoginsFragment import org.mozilla.fenix.settings.registerOnSharedPreferenceChangeListener import java.security.InvalidParameterException @@ -820,36 +819,26 @@ class Settings(private val appContext: Context) : PreferencesHolder { private var savedLoginsSortingStrategyString by stringPreference( appContext.getPreferenceKey(R.string.pref_key_saved_logins_sorting_strategy), - default = SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY + default = SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort.strategyString ) val savedLoginsMenuHighlightedItem: SavedLoginsSortingStrategyMenu.Item - get() { - return when (savedLoginsSortingStrategyString) { - SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY -> { - SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort - } - SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> { - SavedLoginsSortingStrategyMenu.Item.LastUsedSort - } - else -> SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort - } - } + get() = SavedLoginsSortingStrategyMenu.Item.fromString(savedLoginsSortingStrategyString) var savedLoginsSortingStrategy: SortingStrategy get() { - return when (savedLoginsSortingStrategyString) { - SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY -> SortingStrategy.Alphabetically( - appContext.components.publicSuffixList - ) - SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> SortingStrategy.LastUsed - else -> SortingStrategy.Alphabetically(appContext.components.publicSuffixList) + return when (savedLoginsMenuHighlightedItem) { + SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort -> + SortingStrategy.Alphabetically(appContext.components.publicSuffixList) + SavedLoginsSortingStrategyMenu.Item.LastUsedSort -> SortingStrategy.LastUsed } } set(value) { savedLoginsSortingStrategyString = when (value) { - is SortingStrategy.Alphabetically -> SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY - is SortingStrategy.LastUsed -> SavedLoginsFragment.SORTING_STRATEGY_LAST_USED + is SortingStrategy.Alphabetically -> + SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort.strategyString + is SortingStrategy.LastUsed -> + SavedLoginsSortingStrategyMenu.Item.LastUsedSort.strategyString } } } diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index a9bfbf4d2..47bc4e334 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -10,6 +10,7 @@ 112dp 314dp 8dp + 8dp 7dp 56dp 16dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9b89fb6da..f71bd9753 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -250,6 +250,9 @@ +