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! ** diff --git a/app/build.gradle b/app/build.gradle index ad79842d8..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 @@ -469,7 +470,8 @@ 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 implementation Deps.mozilla_lib_push_firebase diff --git a/app/metrics.yaml b/app/metrics.yaml index 98cf7473a..5a15283cb 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 @@ -3161,3 +3207,169 @@ 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" + +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: + - 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: "2021-02-01" + 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: + - 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: "2021-02-01" + 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: + - 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: "2021-02-01" + 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: + - 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: "2021-02-01" + +progressive_web_app: + homescreen_tap: + type: event + description: | + A user taps on PWA homescreen icon + bugs: + - https://github.com/mozilla-mobile/fenix/issues/10261 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/11859 + notification_emails: + - fenix-core@mozilla.com + - erichards@mozilla.com + expires: "2021-03-01" + install_tap: + type: event + description: | + A user installs a PWA. Could be a shortcut or added to homescreen. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/10261 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/11859 + notification_emails: + - fenix-core@mozilla.com + - erichards@mozilla.com + expires: "2021-03-01" + foreground: + type: event + description: | + A user brings the PWA into the foreground. + extra_keys: + time_ms: + description: | + The current time in ms when the PWA was brought to the foreground. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/10261 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/11859 + notification_emails: + - fenix-core@mozilla.com + - erichards@mozilla.com + expires: "2021-03-01" + background: + type: event + description: | + A user puts the PWA into the background. + extra_keys: + time_ms: + description: | + The current time in ms when the PWA was backgrounded. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/10261 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/11859 + notification_emails: + - fenix-core@mozilla.com + - erichards@mozilla.com + expires: "2021-03-01" diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt index 7dd20c944..05b3b82fc 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt @@ -10,6 +10,7 @@ import okhttp3.mockwebserver.MockWebServer import org.junit.Rule import org.junit.Before import org.junit.After +import org.junit.Ignore import org.junit.Test import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.HomeActivityTestRule @@ -74,6 +75,7 @@ class SettingsAddonsTest { } } + @Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13220") // Opens the addons settings menu, installs an addon, then uninstalls @Test fun verifyAddonsCanBeUninstalled() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt index 54c5b1d95..802a3a22b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt @@ -63,6 +63,7 @@ class SmokeTest { } } + @Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13217") @Test fun verifyPageMainMenuItemsListInPortraitNormalModeTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -140,6 +141,7 @@ class SmokeTest { } } + @Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13217") @Test fun verifyPageMainMenuItemsListInPortraitPrivateModeTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) 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/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/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index a932c4089..a03672372 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 @@ -66,8 +67,10 @@ 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 import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.resetPoliciesAfter import org.mozilla.fenix.ext.settings @@ -96,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 /** @@ -243,12 +245,28 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { } } } + + // Launch this on a background thread so as not to affect startup performance + lifecycleScope.launch(IO) { + if ( + settings().isDefaultBrowser() && + 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/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)!!) - } } 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..14ab0713c 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 @@ -104,7 +103,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 @@ -169,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) } @@ -379,8 +375,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( @@ -392,7 +388,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( @@ -617,12 +613,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 ) } } @@ -743,7 +739,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session } @CallSuper - final override fun onPause() { + override fun onPause() { super.onPause() if (findNavController().currentDestination?.id != R.id.searchFragment) { view?.hideKeyboard() @@ -835,14 +831,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 } } 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/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt similarity index 70% 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 a175f0627..83d5ef93d 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 @@ -5,32 +5,11 @@ package org.mozilla.fenix.components.metrics import android.content.Context -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.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 import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.GleanMetrics.ContextMenu import org.mozilla.fenix.GleanMetrics.CrashReporter @@ -38,13 +17,12 @@ 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.ProgressiveWebApp 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 { @@ -176,12 +154,19 @@ sealed class Event { object SearchWidgetCFRCanceled : Event() object SearchWidgetCFRNotNowPressed : Event() object SearchWidgetCFRAddWidgetPressed : Event() + object SearchWidgetInstalled : Event() object OnboardingAutoSignIn : Event() object OnboardingManualSignIn : Event() object OnboardingPrivacyNotice : Event() object OnboardingPrivateBrowsing : Event() object OnboardingWhatsNew : 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() @@ -201,7 +186,21 @@ sealed class Event { object TabsTrayShareAllTabsPressed : Event() object TabsTrayCloseAllTabsPressed : Event() + object ProgressiveWebAppOpenFromHomescreenTap : Event() + object ProgressiveWebAppInstallAsShortcut : Event() + // Interaction events with extras + + data class ProgressiveWebAppForeground(val timeForegrounded: Long) : Event() { + override val extras: Map? + get() = mapOf(ProgressiveWebApp.foregroundKeys.timeMs to timeForegrounded.toString()) + } + + data class ProgressiveWebAppBackground(val timeBackgrounded: Long) : Event() { + override val extras: Map? + get() = mapOf(ProgressiveWebApp.backgroundKeys.timeMs to timeBackgrounded.toString()) + } + data class OnboardingToolbarPosition(val position: Position) : Event() { enum class Position { TOP, BOTTOM } @@ -505,205 +504,19 @@ 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>? get() = null } - -private fun Fact.toEvent(): Event? = when (Pair(component, item)) { - 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; -} - -interface MetricsService { - val type: MetricServiceType - - fun start() - fun stop() - 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() - } - } -} - -private class DebugMetricController : 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") - } -} - -private 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 - } - - 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() - } -} 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..db2e02180 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 @@ -24,6 +25,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 @@ -34,6 +36,7 @@ import org.mozilla.fenix.GleanMetrics.Pocket import org.mozilla.fenix.GleanMetrics.Preferences import org.mozilla.fenix.GleanMetrics.PrivateBrowsingMode import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut +import org.mozilla.fenix.GleanMetrics.ProgressiveWebApp import org.mozilla.fenix.GleanMetrics.QrScanner import org.mozilla.fenix.GleanMetrics.ReaderMode import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine @@ -44,6 +47,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 @@ -140,6 +144,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) } ) @@ -620,40 +636,61 @@ 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) } + ) + is Event.ProgressiveWebAppOpenFromHomescreenTap -> EventWrapper( + { ProgressiveWebApp.homescreenTap.record(it) } + ) + is Event.ProgressiveWebAppInstallAsShortcut -> EventWrapper( + { ProgressiveWebApp.installTap.record(it) } + ) + is Event.ProgressiveWebAppForeground -> EventWrapper( + { ProgressiveWebApp.foreground.record(it) }, + { ProgressiveWebApp.foregroundKeys.valueOf(it) } + ) + is Event.ProgressiveWebAppBackground -> EventWrapper( + { ProgressiveWebApp.background.record(it) }, + { ProgressiveWebApp.backgroundKeys.valueOf(it) } ) // Don't record other events in Glean: @@ -665,6 +702,8 @@ private val Event.wrapper: EventWrapper<*>? is Event.DismissedOnboarding -> null 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 fd00fd9ef..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? @@ -39,6 +40,9 @@ 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" + is Event.ChangedToDefaultBrowser -> "E_Changed_Default_To_Fenix" + is Event.TrackingProtectionSettingChanged -> "E_Changed_ETP" // Do not track other events in Leanplum else -> null @@ -80,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 } @@ -167,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" @@ -178,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 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..16b47a519 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt @@ -0,0 +1,234 @@ +/* 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.feature.pwa.ProgressiveWebAppFacts +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 + } + Component.FEATURE_PWA to ProgressiveWebAppFacts.Items.HOMESCREEN_ICON_TAP -> { + Event.ProgressiveWebAppOpenFromHomescreenTap + } + Component.FEATURE_PWA to ProgressiveWebAppFacts.Items.INSTALL_SHORTCUT -> { + Event.ProgressiveWebAppInstallAsShortcut + } + else -> null + } +} 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 +} diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt index ea853bd94..2387b9c58 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.customtabs import android.content.Context import android.content.Intent +import android.os.SystemClock import android.view.View import androidx.core.view.isVisible import androidx.navigation.fragment.navArgs @@ -30,7 +31,9 @@ import org.mozilla.fenix.R import org.mozilla.fenix.browser.BaseBrowserFragment import org.mozilla.fenix.browser.CustomTabContextMenuCandidate import org.mozilla.fenix.browser.FenixSnackbarDelegate +import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings @@ -150,6 +153,22 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler } } + override fun onResume() { + super.onResume() + val currTimeMs = SystemClock.elapsedRealtimeNanos() / MS_PRECISION + requireComponents.analytics.metrics.track( + Event.ProgressiveWebAppForeground(currTimeMs) + ) + } + + override fun onPause() { + super.onPause() + val currTimeMs = SystemClock.elapsedRealtimeNanos() / MS_PRECISION + requireComponents.analytics.metrics.track( + Event.ProgressiveWebAppBackground(currTimeMs) + ) + } + override fun removeSessionIfNeeded(): Boolean { return customTabsIntegration.onBackPressed() || super.removeSessionIfNeeded() } @@ -192,4 +211,9 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler view, FenixSnackbarDelegate(view) ) + + companion object { + // We only care about millisecond precision for telemetry events + internal const val MS_PRECISION = 1_000_000L + } } 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/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/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/SessionControlAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index c9441f99e..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 @@ -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,24 @@ 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 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) ?: return false + if (newTopSites.topSites.size != this.topSites.size) return false + val newSitesSequence = newTopSites.topSites.asSequence() + val oldTopSites = this.topSites.asSequence() + return newSitesSequence.zip(oldTopSites).all { (new, old) -> new.title == old.title } + } + } + object PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID) object NoCollectionsMessage : AdapterItem(NoCollectionsMessageViewHolder.LAYOUT_ID) @@ -48,32 +63,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 +116,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 +145,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() } } 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/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/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/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 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/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/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 } 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/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/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/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/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/controller/SavedLoginsStorageController.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/controller/SavedLoginsStorageController.kt index 7df5dd741..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 @@ -4,10 +4,10 @@ package org.mozilla.fenix.settings.logins.controller -import android.content.Context 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 @@ -18,8 +18,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 +29,20 @@ 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 val loginsFragmentStore: LoginsFragmentStore, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO ) { - 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) { + val deleteJob = viewLifecycleScope.launch(ioDispatcher) { deleteLoginJob = async { - context.components.core.passwordsStorage.delete(loginId) + passwordsStorage.delete(loginId) } deleteLoginJob?.await() withContext(Dispatchers.Main) { @@ -58,10 +58,10 @@ 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 = 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 +95,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 + ) } } } @@ -121,10 +125,10 @@ 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) - context.components.core.passwordsStorage.getPotentialDupesIgnoringUsername(login!!) + passwordsStorage.getPotentialDupesIgnoringUsername(login!!) } val fetchedDuplicatesList = deferredLogin?.await() fetchedDuplicatesList?.let { list -> @@ -147,9 +151,9 @@ open class SavedLoginsStorageController( fun fetchLoginDetails(loginId: String) { var deferredLogin: Deferred>? = null - val fetchLoginJob = viewLifecycleScope.launch(Dispatchers.IO) { + val fetchLoginJob = viewLifecycleScope.launch(ioDispatcher) { deferredLogin = async { - context.components.core.passwordsStorage.list() + passwordsStorage.list() } val fetchedLoginList = deferredLogin?.await() @@ -175,9 +179,9 @@ open class SavedLoginsStorageController( fun handleLoadAndMapLogins() { var deferredLogins: Deferred>? = null - val fetchLoginsJob = viewLifecycleScope.launch(Dispatchers.IO) { + val fetchLoginsJob = viewLifecycleScope.launch(ioDispatcher) { 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/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/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..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 @@ -101,7 +99,7 @@ class SavedLoginsFragment : Fragment() { ) savedLoginsStorageController = SavedLoginsStorageController( - context = requireContext(), + passwordsStorage = requireContext().components.core.passwordsStorage, viewLifecycleScope = viewLifecycleOwner.lifecycleScope, navController = findNavController(), loginsFragmentStore = savedLoginsStore @@ -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/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/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/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingDialogFragment.kt index 43b9099fc..05d8520e3 100644 --- a/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingDialogFragment.kt @@ -38,7 +38,9 @@ class PwaOnboardingDialogFragment : DialogFragment() { add_button.setOnClickListener { viewLifecycleOwner.lifecycleScope.launch { components.useCases.webAppUseCases.addToHomescreen() - }.invokeOnCompletion { dismiss() } + }.invokeOnCompletion { + dismiss() + } } } } 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..e51419df4 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,12 @@ 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.TabViewHolder import mozilla.components.browser.storage.sync.Tab as SyncTab import mozilla.components.concept.sync.Device as SyncDevice @@ -24,6 +26,7 @@ class SyncedTabsAdapter( return when (viewType) { DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView) TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView) + ErrorViewHolder.LAYOUT_ID -> ErrorViewHolder(itemView) else -> throw IllegalStateException() } } @@ -33,8 +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.Device -> DeviceViewHolder.LAYOUT_ID + is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID + is AdapterItem.Error -> ErrorViewHolder.LAYOUT_ID } fun updateData(syncedTabs: List) { @@ -55,7 +59,7 @@ class SyncedTabsAdapter( when (oldItem) { is AdapterItem.Device -> newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id - is AdapterItem.Tab -> + is AdapterItem.Tab, is AdapterItem.Error -> oldItem == newItem } @@ -67,5 +71,9 @@ class SyncedTabsAdapter( sealed class AdapterItem { data class Device(val device: SyncDevice) : AdapterItem() data class Tab(val tab: SyncTab) : 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 0889ccb50..8308692ba 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,11 @@ 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.NavController +import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.component_sync_tabs.view.* import kotlinx.coroutines.CoroutineScope @@ -17,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, @@ -43,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) } @@ -54,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 } @@ -78,6 +83,7 @@ class SyncedTabsLayout @JvmOverloads constructor( } companion object { + 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. @@ -94,9 +100,23 @@ 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 } + + internal 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 bcd494cf0..95abada88 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.view.View.VISIBLE +import android.widget.LinearLayout import androidx.recyclerview.widget.RecyclerView +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 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,29 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item } } + class ErrorViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { + + override fun bind(item: T, interactor: (Tab) -> Unit) { + val errorItem = item as AdapterItem.Error + setErrorMargins() + + 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.sync_tabs_error_row + } + } + class DeviceViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { override fun bind(item: T, interactor: (Tab) -> Unit) { @@ -45,18 +74,37 @@ 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 } 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 { 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/java/org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt index 433c620e1..96e130079 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.tab_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..c7e2e3004 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 -> @@ -73,7 +70,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..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,30 +5,37 @@ 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.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 class TabHistoryViewHolder( - private val view: View, + view: View, private val interactor: TabHistoryViewInteractor -) : RecyclerView.ViewHolder(view) { +) : ViewHolder(view) { + + private lateinit var item: TabHistoryItem + + init { + history_layout.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) { - buildSpannedString { - bold { append(item.title) } - } + 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) + + if (item.isSelected) { + history_layout.setBackgroundColor(history_layout.context.getColorFromAttr(R.attr.tabHistoryItemSelectedBackground)) } else { - item.title + history_layout.background = null } - - view.setOnClickListener { interactor.goToHistoryItem(item) } } } 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..03d10be28 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 @@ -19,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 { @@ -31,7 +33,26 @@ class SaveToCollectionsButtonAdapter( return ViewHolder(itemView, interactor) } - override fun onBindViewHolder(holder: ViewHolder, position: Int) = Unit + override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList) { + if (payloads.isNullOrEmpty()) { + onBindViewHolder(holder, position) + return + } + + 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 = !isPrivate && + interactor.onModeRequested() is TabTrayDialogFragmentState.Mode.Normal + } override fun getItemViewType(position: Int): Int { return ViewHolder.LAYOUT_ID @@ -43,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/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/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt index a3ccf9ed9..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 @@ -70,8 +71,9 @@ 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) + private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate) private var hasLoaded = false @@ -83,7 +85,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 +102,9 @@ class TabTrayView( interactor.onTabTrayDismissed() } } - }) + } + + behavior.addBottomSheetCallback(bottomSheetCallback) val selectedTabIndex = if (!isPrivate) { DEFAULT_TAB_ID @@ -230,9 +234,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) @@ -257,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( @@ -273,6 +289,7 @@ class TabTrayView( view.tabsTray.apply { tabsTouchHelper.attachToRecyclerView(this) } + behavior.addBottomSheetCallback(bottomSheetCallback) toggleUIMultiselect(multiselect = false) @@ -281,6 +298,7 @@ class TabTrayView( is TabTrayDialogFragmentState.Mode.MultiSelect -> { // Disable swipe to delete while in multiselect tabsTouchHelper.attachToRecyclerView(null) + behavior.removeBottomSheetCallback(bottomSheetCallback) toggleUIMultiselect(multiselect = true) @@ -402,13 +420,18 @@ 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 ) - this.adapter?.notifyItemRangeChanged(0, tabs.size, true) + collectionsButtonAdapter.notifyItemChanged( + 0, + if (inMultiselectMode) MultiselectModeChange.MULTISELECT else MultiselectModeChange.NORMAL + ) + + tabsAdapter.notifyItemRangeChanged(0, tabs.size, true) } } @@ -420,7 +443,7 @@ class TabTrayView( val selectedBrowserTabIndex = tabs.indexOfFirst { it.id == itemId } - this.adapter?.notifyItemChanged( + tabsAdapter.notifyItemChanged( selectedBrowserTabIndex, true ) } 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 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/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index defa8901d..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 @@ -324,6 +323,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 @@ -811,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/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( 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) { 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/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"> + + + tools:listheader="@layout/exceptions_description" + tools:listitem="@layout/exception_item" + tools:listfooter="@layout/delete_exceptions_button" /> 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" /> - - - 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/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/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/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/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" /> - + - - 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" /> %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-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index f266d3ab8..a828debb0 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -26,6 +26,19 @@ %1$s otevřených panelů. Klepnutím přepnete panely. + + Vybráno panelů: %1$d + + Přidat novou sbírku + + Název + + Vybrat sbírku + + Uložit vybrané panely do sbírky + + Vybráno + Autorem aplikace %1$s je Mozilla. @@ -43,7 +56,7 @@ - Přidejte si zkratku k otevření anonymního prohlížení na plochu. + Vytvořte si zástupce pro otevření anonymního panelu z domovské obrazovky. Přidat zkratku @@ -151,8 +164,12 @@ Naskenovat + + Vyhledávač Nastavení vyhledávače + + Vyhledat pomocí: Vložit odkaz ze schránky @@ -261,6 +278,8 @@ Nástroje pro vývojáře Vzdálené ladění pomocí USB + + Zobrazovat vyhledávače Našeptávat vyhledávání @@ -519,6 +538,9 @@ %1$s (anonymní režim) + + Uložit + Vymazat historii @@ -585,6 +607,8 @@ Vybrat složku Opravdu chcete smazat tuto složku? + + %s smaže vybrané položky. Složka %1$s smazána @@ -643,6 +667,8 @@ Záložky smazány + + Mazání vybraných složek ZPĚT @@ -737,6 +763,8 @@ Vybrán jeden panel Panely uloženy + + Sbírka uložena Panel uložen @@ -1278,6 +1306,8 @@ Kopírovat uživatelské jméno Kopírovat server + + Otevřít v prohlížeči Zobrazit heslo 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-da/strings.xml b/app/src/main/res/values-da/strings.xml index c21238321..becc64e31 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 @@ -206,7 +228,7 @@ Betalingskort og adresser - Angiv som standard-browser + Angiv som standardbrowser Avanceret @@ -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-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-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-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-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9a397b014..d4861c785 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -27,6 +27,29 @@ %1$s onglets ouverts. Appuyez pour changer d’onglet. + + %1$d sélectionnés + + Créer une nouvelle collection + + Nom + + Sélectionner une collection + + Quitter le mode de sélection multiple + + Enregistrer les onglets sélectionnés dans une collection + + %1$s sélectionné + + %1$s désélectionné + + Sortie du mode de sélection multiple + + Mode de sélection multiple activé, sélectionnez les onglets à enregistrer dans une collection + + Sélectionné + %1$s est réalisé par Mozilla. @@ -518,6 +541,9 @@ %1$s (navigation privée) + + Enregistrer + Effacer l’historique 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-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-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 2ba635e9c..7fae0ad27 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,10 +281,10 @@ 開発者ツール 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-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index b082b3a74..cb7f8862b 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -36,6 +36,21 @@ 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 + %1$s d afares n Mozilla. 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-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 6997a10e0..59a446665 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -185,7 +185,7 @@ 검색 엔진 설정 - 이번에 사용할 검색 엔진: + 이번만 검색: 클립보드로부터 링크 채우기 @@ -289,7 +289,7 @@ 데이터 수집 - 개인 정보 정책 + 개인정보 보호정책 개발자 도구 @@ -1083,7 +1083,7 @@      - 개인 정보 보호 정책 읽기 + 개인정보 보호정책 읽기 닫기 @@ -1237,7 +1237,7 @@ 충돌 - 개인 정보 보호 정책 + 개인정보 보호정책 권리 읽기 @@ -1538,7 +1538,7 @@ 검색 - 이번에 사용할 검색 엔진: + 이번만 검색: 검색 바로 가기 표시 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-night/colors.xml b/app/src/main/res/values-night/colors.xml index 8fc545e42..29193af41 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -50,6 +50,9 @@ @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 @@ -64,9 +67,10 @@ @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_background_dark_theme - @color/top_site_border_dark_theme @color/top_site_title_text_dark_theme 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-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-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-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 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-th/strings.xml b/app/src/main/res/values-th/strings.xml index 90cd8a840..9dca54614 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -24,6 +24,29 @@ %1$s แท็บที่เปิด แตะเพื่อสลับไปยังแท็บ + + เลือกอยู่ %1$d + + เพิ่มชุดสะสมใหม่ + + ชื่อ + + เลือกชุดสะสม + + ออกจากโหมดเลือกหลายรายการ + + บันทึกแท็บที่เลือกลงในชุดสะสม + + เลือกแล้ว %1$s + + เลิกเลือกแล้ว %1$s + + ออกจากโหมดเลือกหลายรายการแล้ว + + เข้าสู่โหมดเลือกหลายรายการแล้ว เลือกแท็บที่จะบันทึกลงในชุดสะสม + + เลือกแล้ว + %1$s ผลิตขึ้นโดย Mozilla @@ -147,13 +170,11 @@ สแกน - ทางลัด + เครื่องมือค้นหา การตั้งค่าเครื่องมือค้นหา - - ค้นหาด้วย - คราวนี้ค้นหาด้วย: + คราวนี้ค้นหาด้วย: เติมลิงก์จากคลิปบอร์ด @@ -262,8 +283,8 @@ เครื่องมือนักพัฒนา การดีบั๊กระยะไกลผ่าน USB - - แสดงทางลัดการค้นหา + + แสดงเครื่องมือค้นหา แสดงข้อเสนอแนะการค้นหา @@ -511,6 +532,9 @@ %1$s (โหมดส่วนตัว) + + บันทึก + ลบประวัติ @@ -576,6 +600,8 @@ เลือกโฟลเดอร์ คุณแน่ใจหรือไม่ว่าต้องการลบโฟลเดอร์นี้? + + %s จะลบรายการที่เลือก ลบ %1$s แล้ว @@ -630,8 +656,10 @@ ลบ %1$s แล้ว - + เอาที่คั่นหน้าออกแล้ว + + กำลังลบโฟลเดอร์ที่เลือก เลิกทำ @@ -724,6 +752,8 @@ เลือกอยู่ %d แท็บ บันทึกแท็บแล้ว! + + บันทึกชุดสะสมแล้ว! บันทึกแท็บแล้ว! @@ -828,6 +858,10 @@ ปฏิเสธ คุณแน่ใจหรือไม่ว่าต้องการลบ %1$s? + + การลบแท็บนี้จะลบชุดสะสมทั้งชุด คุณสามารถสร้างชุดสะสมใหม่ได้ทุกเมื่อ + + ลบ %1$s หรือไม่? ลบ @@ -1227,6 +1261,8 @@ การเข้าสู่ระบบและรหัสผ่านที่ไม่ได้บันทึกจะถูกแสดงที่นี่ การเข้าสู่ระบบและรหัสผ่านจะไม่ถูกบันทึกสำหรับไซต์เหล่านี้ + + ลบข้อยกเว้นทั้งหมด ค้นหาการเข้าสู่ระบบ @@ -1265,6 +1301,8 @@ คัดลอกชื่อผู้ใช้ คัดลอกไซต์ + + เปิดไซต์ในเบราว์เซอร์ แสดงรหัสผ่าน @@ -1440,4 +1478,13 @@ ตกลง เข้าใจแล้ว - + + + ทางลัด + + ค้นหาด้วย + + คราวนี้ค้นหาด้วย: + + แสดงทางลัดการค้นหา + 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 змінено. Вихід з програми для застосування змін… Обліковий запис diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index 082743f14..a1f99455b 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,405 @@ 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 + + + kamera + + Mikrofon + + Manzil + + Bildirishnoma + + Ruxsat berishni soʻrang + + Bloklandi + + Ruxsat berildi + + Android tomonidan bloklangan + + Istisnolar + + Yoniq + + Oʻchiq + + Audio va videoga ruxsat berish + + Audio va video faqat mobil internetda bloklansin + + Audio va video faqat Wi-Fi yoniqligida ijro etilsin + + Faqat audioni bloklash + + Audio va videoni bloklash + + Yoniq + + Oʻchiq + + + + Kolleksiyalar + + Kolleksiya menyusi + + Siz uchun muhim boʻlgan narsalarni toʻplang + + Oʻxshash qidiruv, sayt va varaqlarga tezkor kirish uchun ularni bir joyga jamlang. + + Yorliqlarni tanlash + + Kolleksiyani tanlash + + Kolleksiya nomi + + Yangi kolleksiya qoʻshish + + Barchasini tanlash + + Barcha belgilashni bekor qilish + + Saqlash uchun varaqlarni tanlang + + %d ta varaq tanlandi + + + %d ta varaq tanlandi + + Varaqlar saqlandi! + + Kolleksiya saqlandi! + + Varaq saqlandi! + + Yopish + + Saqlash + + Koʻrinishi + + + Kolleksiya %d + + + + Yuborish va ulashish + + Ulashish + + Ulashish + + Havolani ulashish + + Qurilmaga joʻnatish + + Barcha amallar + + Yaqinda ishlatilgan + + Sinxronlash uchun hisobingizga kiring + + Barcha qurilmalarga joʻnatish + + Sinxronizatsiyaga qayta ulanish + + Oflayn + + Boshqa qurilmani ulash + + Varaqni yuborish uchun kamida bitta boshqa qurilmada Firefox hisobiga kirishingiz kerak. + + Tushundim! + + Bu ilovaga ulashilmadi + + Qurilmaga joʻnatish + + Hech qanday qurilma ulanmagan + + Varaqlarni yuborish haqida… + + Boshqa qurilmani ulash… + + + + Maxfiy koʻrish seansi + + Maxfiy varaqlarni oʻchirish + + Maxfiy varaqlarni yopish + + Ochish + + Oʻchirish va ochish + + + Asosi: + + Kolleksiya oʻchirildi + + Kolleksiya nomi oʻzgardi + + Varaq oʻchirildi + + Varaqlar oʻchirildi + + Varaq yopildi + + Varaqlar yopildi + Koʻchirish tugadi diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8bc09e8ae..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) 搜索语言 @@ -282,7 +282,7 @@ 重新连接以恢复同步 - 语言(地区) + 语言 数据反馈 @@ -320,7 +320,7 @@ 立即同步 - 选择要同步的信息 + 选择要同步的项目 历史记录 @@ -751,7 +751,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 個分頁 新增收藏集 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 9a22639e5..3dbeaf224 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,21 +59,27 @@ #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 + + @color/tab_tray_item_selected_background_light_theme + #FBFBFE #A7A2B7 @@ -108,10 +99,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,27 +126,33 @@ #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 + + @color/tab_tray_item_selected_background_dark_theme + #FBFBFE #A7A2B7 @color/primary_text_private_theme #261E4B - #291D4F - #291D4F + @color/photonInk50 + @color/photonInk50 #9059FF #592ACB #AB71FF @@ -192,17 +187,23 @@ #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 + + @color/tab_tray_item_selected_background_dark_theme + @color/primary_text_light_theme @color/secondary_text_light_theme @@ -245,6 +246,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 @@ -259,12 +263,13 @@ @color/tab_tray_item_thumbnail_icon_light_theme @color/tab_tray_selected_mask_light_theme + + @color/tab_history_item_selected_background_light_theme + #DFDFE3 - @color/top_site_background_light_theme - @color/top_site_border_light_theme @color/top_site_title_text_light_theme @@ -330,8 +335,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/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/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 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 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e14bfecf9..0047a3592 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1432,9 +1432,7 @@ A login with that username already exists - - Connect with a Firefox Account. - + Connect another device. Please re-authenticate. @@ -1455,15 +1453,4 @@ OK, Got It - - - Shortcuts - - Search with - - This time, search with: - - Show search shortcuts - - Search Engine diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a11de8c72..f71bd9753 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 @@ -89,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 @@ -213,6 +220,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 @@ -223,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 @@ -236,6 +250,9 @@ +