1
0
Fork 0

Merge branch 'master' of https://github.com/mozilla-mobile/fenix
continuous-integration/drone/push Build is passing Details

master
blallo 2020-08-03 23:50:16 +02:00
commit 4e2a9d6999
184 changed files with 4742 additions and 2210 deletions

View File

@ -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! **

View File

@ -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

View File

@ -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"

View File

@ -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() {

View File

@ -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)

View File

@ -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

View File

@ -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() {

View File

@ -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

View File

@ -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)!!)
}
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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<ProgressiveWebApp.foregroundKeys, String>?
get() = mapOf(ProgressiveWebApp.foregroundKeys.timeMs to timeForegrounded.toString())
}
data class ProgressiveWebAppBackground(val timeBackgrounded: Long) : Event() {
override val extras: Map<ProgressiveWebApp.backgroundKeys, String>?
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<Autoplay.settingChangedKeys, String>?
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<MetricsService>,
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<MetricsService>,
private val isDataTelemetryEnabled: () -> Boolean,
private val isMarketingDataTelemetryEnabled: () -> Boolean
) : MetricController {
private var initialized = mutableSetOf<MetricServiceType>()
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()
}
}

View File

@ -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<NoExtraKeys>(
{ LoginDialog.displayed.record(it) }
)
is Event.LoginDialogPromptCancelled -> EventWrapper<NoExtraKeys>(
{ LoginDialog.cancelled.record(it) }
)
is Event.LoginDialogPromptSave -> EventWrapper<NoExtraKeys>(
{ LoginDialog.saved.record(it) }
)
is Event.LoginDialogPromptNeverSave -> EventWrapper<NoExtraKeys>(
{ LoginDialog.neverSave.record(it) }
)
is Event.FindInPageOpened -> EventWrapper<NoExtraKeys>(
{ FindInPage.opened.record(it) }
)
@ -620,40 +636,61 @@ private val Event.wrapper: EventWrapper<*>?
)
is Event.TabsTrayOpened -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.opened.record(it) }
{ TabsTray.opened.record(it) }
)
is Event.TabsTrayClosed -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.closed.record(it) }
{ TabsTray.closed.record(it) }
)
is Event.OpenedExistingTab -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.openedExistingTab.record(it) }
{ TabsTray.openedExistingTab.record(it) }
)
is Event.ClosedExistingTab -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.closedExistingTab.record(it) }
{ TabsTray.closedExistingTab.record(it) }
)
is Event.TabsTrayPrivateModeTapped -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.privateModeTapped.record(it) }
{ TabsTray.privateModeTapped.record(it) }
)
is Event.TabsTrayNormalModeTapped -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.normalModeTapped.record(it) }
{ TabsTray.normalModeTapped.record(it) }
)
is Event.NewTabTapped -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.newTabTapped.record(it) }
{ TabsTray.newTabTapped.record(it) }
)
is Event.NewPrivateTabTapped -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.newPrivateTabTapped.record(it) }
{ TabsTray.newPrivateTabTapped.record(it) }
)
is Event.TabsTrayMenuOpened -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.menuOpened.record(it) }
{ TabsTray.menuOpened.record(it) }
)
is Event.TabsTraySaveToCollectionPressed -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.saveToCollection.record(it) }
{ TabsTray.saveToCollection.record(it) }
)
is Event.TabsTrayShareAllTabsPressed -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.shareAllTabs.record(it) }
{ TabsTray.shareAllTabs.record(it) }
)
is Event.TabsTrayCloseAllTabsPressed -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.closeAllTabs.record(it) }
{ TabsTray.closeAllTabs.record(it) }
)
Event.AutoPlaySettingVisited -> EventWrapper<NoExtraKeys>(
{ Autoplay.visitedSetting.record(it) }
)
is Event.AutoPlaySettingChanged -> EventWrapper(
{ Autoplay.settingChanged.record(it) },
{ Autoplay.settingChangedKeys.valueOf(it) }
)
is Event.ProgressiveWebAppOpenFromHomescreenTap -> EventWrapper<NoExtraKeys>(
{ ProgressiveWebApp.homescreenTap.record(it) }
)
is Event.ProgressiveWebAppInstallAsShortcut -> EventWrapper<NoExtraKeys>(
{ 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 {

View File

@ -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

View File

@ -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<MetricsService>,
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<MetricsService>,
private val isDataTelemetryEnabled: () -> Boolean,
private val isMarketingDataTelemetryEnabled: () -> Boolean
) : MetricController {
private var initialized = mutableSetOf<MetricServiceType>()
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
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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<T : Any>(
private val interactor: ExceptionsInteractor<T>,
diffCallback: DiffUtil.ItemCallback<AdapterItem>
) : ListAdapter<ExceptionsAdapter.AdapterItem, RecyclerView.ViewHolder>(diffCallback) {
/**
* Change the list of items that are displayed.
* Header and footer items are added to the list as well.
*/
fun updateData(exceptions: List<T>) {
val adapterItems: List<AdapterItem> = 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<T>
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<T>
val adapterItem = getItem(position) as AdapterItem.Item<T>
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<T> : AdapterItem() {
abstract val item: T
abstract val url: String
}
}
}

View File

@ -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<T> {
/**
* Called whenever all exception items are deleted
*/
fun onDeleteAll()
/**
* Called whenever one exception item is deleted
*/
fun onDeleteOne(item: T)
}

View File

@ -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<T : Any>(
container: ViewGroup,
protected val interactor: ExceptionsInteractor<T>
) : 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<T>
init {
exceptions_list.apply {
layoutManager = LinearLayoutManager(containerView.context)
}
}
fun update(items: List<T>) {
exceptions_empty_view.isVisible = items.isEmpty()
exceptions_list.isVisible = items.isNotEmpty()
exceptionsAdapter.updateData(items)
}
}

View File

@ -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<LoginException>) : State
data class ExceptionsFragmentState(val items: List<LoginException> = emptyList()) : State
/**
* The ExceptionsState Reducer.

View File

@ -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<LoginException>(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<LoginException>() {
override val url get() = item.origin
}
internal object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
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
}
}

View File

@ -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)
}
}
}

View File

@ -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<LoginException>
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)
}
}
}

View File

@ -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<LoginException>(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
}
}
}

View File

@ -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<TrackingProtectionException>) : State
data class ExceptionsFragmentState(val items: List<TrackingProtectionException> = emptyList()) : State
/**
* The ExceptionsState Reducer.

View File

@ -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<TrackingProtectionException>(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<TrackingProtectionException>() {
override val url get() = item.url
}
internal object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
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
}
}

View File

@ -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)
}
}
}

View File

@ -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<TrackingProtectionException> {
/**
* 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)
)
}
}
}

View File

@ -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<TrackingProtectionException>(container, interactor) {
override val exceptionsAdapter = TrackingProtectionExceptionsAdapter(interactor)
init {
exceptions_list.apply {
adapter = exceptionsAdapter
}
with(exceptions_learn_more) {
addUnderline()
movementMethod = LinkMovementMethod.getInstance()
setOnClickListener { interactor.onLearnMore() }
}
}
}

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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<T : Any>(
view: View,
private val interactor: ExceptionsInteractor<T>,
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
}
}

View File

@ -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

View File

@ -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"

View File

@ -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<TopSite>) : AdapterItem(TopSiteViewHolder.LAYOUT_ID)
ButtonTipViewHolder.LAYOUT_ID
)
data class TopSiteList(val topSites: List<TopSite>) : 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<AdapterItem>() {
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()
}
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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<LoginExceptionsAdapter.AdapterItem, RecyclerView.ViewHolder>(DiffCallback) {
/**
* Change the list of items that are displayed.
* Header and footer items are added to the list as well.
*/
fun updateData(exceptions: List<LoginException>) {
val adapterItems: List<AdapterItem> = 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<AdapterItem>() {
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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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<StorageStatsManager>()?.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)
}
}
}

View File

@ -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
}

View File

@ -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()

View File

@ -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) {

View File

@ -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))

View File

@ -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()
}
}

View File

@ -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 {

View File

@ -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<TextMenuCandidate> {
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))
}
}

View File

@ -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)
}
}

View File

@ -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<Boolean>? = 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<Unit>? = 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<List<Login>>? = 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<List<Login>>? = 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<List<Login>>? = 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 {

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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"
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -38,7 +38,9 @@ class PwaOnboardingDialogFragment : DialogFragment() {
add_button.setOnClickListener {
viewLifecycleOwner.lifecycleScope.launch {
components.useCases.webAppUseCases.addToHomescreen()
}.invokeOnCompletion { dismiss() }
}.invokeOnCompletion {
dismiss()
}
}
}
}

View File

@ -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<SyncedDeviceTabs>) {
@ -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()
}
}

View File

@ -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<SyncedTabsFragment>().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<SyncedTabsAdapter.AdapterItem> = 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<SyncedDeviceTabs>) {
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)
}
}
}

View File

@ -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 <T : AdapterItem> 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 <T : AdapterItem> 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
}
}

View File

@ -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<TabHistoryViewHolder>() {
var historyList: List<TabHistoryItem> = emptyList()
set(value) {
field = value
notifyDataSetChanged()
}
) : ListAdapter<TabHistoryItem, TabHistoryViewHolder>(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<TabHistoryItem>() {
override fun areItemsTheSame(oldItem: TabHistoryItem, newItem: TabHistoryItem) =
oldItem.url == newItem.url
override fun areContentsTheSame(oldItem: TabHistoryItem, newItem: TabHistoryItem) =
oldItem == newItem
}
}

View File

@ -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)
}
}
}

View File

@ -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) }
}
}

View File

@ -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<Item, ViewHolder>(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<Any>) {
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.
*/

View File

@ -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<Session>) -> Unit,
private val showAddNewCollectionDialog: (List<Session>) -> 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<Tab>) {
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<Session> {
return activity.components.core.sessionManager.sessionsOfType(private = private).toList()
return sessionManager.sessionsOfType(private = private).toList()
}
override fun onModeRequested(): TabTrayDialogFragmentState.Mode {

View File

@ -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,

View File

@ -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
)
}

View File

@ -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

View File

@ -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<ExceptionsAdapter.AdapterItem, RecyclerView.ViewHolder>(DiffCallback) {
/**
* Change the list of items that are displayed.
* Header and footer items are added to the list as well.
*/
fun updateData(exceptions: List<TrackingProtectionException>) {
val adapterItems = mutableListOf<AdapterItem>()
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<AdapterItem>() {
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
areContentsTheSame(oldItem, newItem)
@Suppress("DiffUtilEquals")
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
oldItem == newItem
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
)
)
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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(

View File

@ -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) {

View File

@ -9,5 +9,5 @@
android:width="14dp"
android:height="14dp" />
<solid android:color="?accentBright" />
<stroke android:color="@color/light_grey_05"/>
<stroke android:color="@color/photonLightGrey05"/>
</shape>

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<size
android:width="40dp"
android:height="40dp"/>
<solid android:color="@color/quick_action_reader_close_icon_background"/>
</shape>
</item>
<item
android:bottom="8dp"
android:left="8dp"
android:right="8dp"
android:top="8dp">
<vector
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13.36 11.98l4.38-4.38a0.95 0.95 0 0 0-1.34-1.34l-4.38 4.38-4.38-4.38A0.95 0.95 0 0 0 6.3 7.6l4.38 4.38-4.38 4.38a0.95 0.95 0 1 0 1.34 1.34l4.38-4.38 4.38 4.38a0.95 0.95 0 0 0 1.34-1.34l-4.38-4.38z"
android:fillColor="@color/quick_action_reader_close_icon"/>
</vector>
</item>
</layer-list>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/quick_action_icon_read" />
<item
android:left="32dp"
android:bottom="32dp">
<shape
android:shape="oval">
<stroke
android:width="1dp"
android:color="@color/light_grey_05" />
<solid android:color="?accentBright" />
<size
android:width="14dp"
android:height="14dp"/>
</shape>
</item>
</layer-list>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="false"
android:drawable="@drawable/quick_action_icon_read" />
<item android:state_selected="true"
android:drawable="@drawable/quick_action_icon_close" />
</selector>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="false"
android:drawable="@drawable/quick_action_icon_read_with_notification" />
<item android:state_selected="true"
android:drawable="@drawable/quick_action_icon_close" />
</selector>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="4dp" />
<solid android:color="@color/top_site_background" />
<stroke android:width="1dp" android:color="@color/top_site_border" />
</shape>

View File

@ -7,6 +7,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/fragment_browser" />
<org.mozilla.fenix.browser.TabPreview
android:id="@+id/tabPreview"
android:layout_width="match_parent"

View File

@ -45,5 +45,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:listitem="@layout/exception_item" />
tools:listheader="@layout/exceptions_description"
tools:listitem="@layout/exception_item"
tools:listfooter="@layout/delete_exceptions_button" />
</FrameLayout>

View File

@ -20,20 +20,6 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/sync_tabs_status"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
android:text="@string/sync_connect_device"
android:textColor="?secondaryText"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/synced_tabs_pull_to_refresh"
android:layout_width="match_parent"

View File

@ -2,7 +2,8 @@
<!-- 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/. -->
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
<com.google.android.material.button.MaterialButton
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/removeAllExceptions"
style="@style/DestructiveButton"
android:layout_marginHorizontal="16dp"

View File

@ -3,10 +3,11 @@
- 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/. -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/exceptions_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:text="@string/enhanced_tracking_protection_exceptions"
tools:text="@string/enhanced_tracking_protection_exceptions"
android:textColor="?primaryText"
android:textSize="16sp" />

View File

@ -12,27 +12,14 @@
android:focusable="true"
android:minHeight="?android:attr/listPreferredItemHeight">
<FrameLayout
android:id="@+id/favicon_wrapper"
android:layout_width="@dimen/history_favicon_width_height"
android:layout_height="@dimen/history_favicon_width_height"
android:padding="@dimen/saved_logins_item_padding"
android:layout_marginStart="@dimen/saved_logins_item_margin_start"
android:background="@drawable/top_sites_background"
android:layout_gravity="center"
android:importantForAccessibility="noHideDescendants"
<ImageView
android:id="@+id/favicon_image"
style="@style/Mozac.Widgets.Favicon"
android:layout_marginStart="16dp"
android:importantForAccessibility="no"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<ImageView
android:id="@+id/favicon_image"
android:layout_width="@dimen/preference_icon_drawable_size"
android:layout_height="@dimen/preference_icon_drawable_size"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:importantForAccessibility="no"
android:scaleType="fitCenter" />
</FrameLayout>
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/webAddressView"
@ -46,7 +33,7 @@
android:textColor="?primaryText"
app:layout_constraintBottom_toTopOf="@id/usernameView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/favicon_wrapper"
app:layout_constraintStart_toEndOf="@+id/favicon_image"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="mozilla.org" />
@ -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" />

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@android:id/title"
style="?android:attr/listViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:gravity="start|center_vertical"
android:textAlignment="viewStart"
app:fontFamily="@font/metropolis_semibold" />

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/empty_session_control_background"
android:layout_marginBottom="12dp"
android:padding="16dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/sync_tabs_error_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="14sp"
android:textAlignment="viewStart"
tools:text="@string/synced_tabs_no_tabs"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/sync_tabs_error_cta_button"
style="@style/PositiveButton"
app:icon="@drawable/ic_sign_in"
android:visibility="gone"
android:text="@string/synced_tabs_sign_in_button"
android:layout_marginTop="8dp"/>
</LinearLayout>

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