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) [![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) [![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! ** ** 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_domains
implementation Deps.mozilla_browser_icons implementation Deps.mozilla_browser_icons
implementation Deps.mozilla_browser_menu implementation Deps.mozilla_browser_menu
implementation Deps.mozilla_browser_menu2
implementation Deps.mozilla_browser_search implementation Deps.mozilla_browser_search
implementation Deps.mozilla_browser_session implementation Deps.mozilla_browser_session
implementation Deps.mozilla_browser_state implementation Deps.mozilla_browser_state
@ -469,7 +470,8 @@ dependencies {
implementation Deps.mozilla_ui_colors implementation Deps.mozilla_ui_colors
implementation Deps.mozilla_ui_icons 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_crash
implementation Deps.mozilla_lib_push_firebase implementation Deps.mozilla_lib_push_firebase

View File

@ -471,6 +471,52 @@ context_menu:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2020-10-01" 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: find_in_page:
opened: opened:
type: event type: event
@ -3161,3 +3207,169 @@ perf.awesomebar:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- gkruglov@mozilla.com - gkruglov@mozilla.com
expires: "2020-10-01" 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.Rule
import org.junit.Before import org.junit.Before
import org.junit.After import org.junit.After
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule 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 // Opens the addons settings menu, installs an addon, then uninstalls
@Test @Test
fun verifyAddonsCanBeUninstalled() { fun verifyAddonsCanBeUninstalled() {

View File

@ -63,6 +63,7 @@ class SmokeTest {
} }
} }
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13217")
@Test @Test
fun verifyPageMainMenuItemsListInPortraitNormalModeTest() { fun verifyPageMainMenuItemsListInPortraitNormalModeTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -140,6 +141,7 @@ class SmokeTest {
} }
} }
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13217")
@Test @Test
fun verifyPageMainMenuItemsListInPortraitPrivateModeTest() { fun verifyPageMainMenuItemsListInPortraitPrivateModeTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

View File

@ -6,9 +6,7 @@ package org.mozilla.fenix
enum class ReleaseChannel { enum class ReleaseChannel {
FenixDebug, FenixDebug,
FenixProduction, FenixProduction,
FennecProduction, FennecProduction,
FennecBeta; FennecBeta;
@ -35,6 +33,12 @@ enum class ReleaseChannel {
else -> false else -> false
} }
val isRelease: Boolean
get() = when (this) {
FennecProduction -> true
else -> false
}
val isBeta: Boolean val isBeta: Boolean
get() = when (this) { get() = when (this) {
FennecBeta -> true 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.components.metrics.MetricServiceType
import org.mozilla.fenix.ext.resetPoliciesAfter import org.mozilla.fenix.ext.resetPoliciesAfter
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StorageStatsMetrics
import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.push.PushFxaIntegration import org.mozilla.fenix.push.PushFxaIntegration
import org.mozilla.fenix.push.WebPushEngineIntegration 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() initQueue()
// We init these items in the visual completeness queue to avoid them initing in the critical // 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). // startup path, before the UI finishes drawing (i.e. visual completeness).
queueInitExperiments() queueInitExperiments()
queueInitStorageAndServices() queueInitStorageAndServices()
queueMetrics()
} }
private fun startMetricsIfEnabled() { private fun startMetricsIfEnabled() {

View File

@ -31,6 +31,7 @@ import androidx.navigation.ui.NavigationUI
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay 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.browser.browsingmode.DefaultBrowsingModeManager
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.components.metrics.Event 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.alreadyOnDestination
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.resetPoliciesAfter import org.mozilla.fenix.ext.resetPoliciesAfter
import org.mozilla.fenix.ext.settings 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.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.trackingprotectionexceptions.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.utils.BrowsersCache 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() { final override fun onPause() {
if (settings().lastKnownMode.isPrivate) { if (settings().lastKnownMode.isPrivate) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} }
if (settings().wasDefaultBrowserOnLastPause != settings().isDefaultBrowser()
) {
settings().wasDefaultBrowserOnLastPause = settings().isDefaultBrowser()
}
super.onPause() super.onPause()
// Every time the application goes into the background, it is possible that the user // 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 kotlinx.android.synthetic.main.fragment_add_on_details.*
import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.translatedDescription import mozilla.components.feature.addons.ui.translatedDescription
import mozilla.components.feature.addons.ui.updatedAtDate
import org.mozilla.fenix.R import org.mozilla.fenix.R
import java.text.DateFormat import java.text.DateFormat
import java.text.NumberFormat import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
interface AddonDetailsInteractor { interface AddonDetailsInteractor {
@ -44,7 +44,6 @@ class AddonDetailsView(
private val interactor: AddonDetailsInteractor private val interactor: AddonDetailsInteractor
) : LayoutContainer { ) : LayoutContainer {
private val dateParser = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
private val dateFormatter = DateFormat.getDateInstance() private val dateFormatter = DateFormat.getDateInstance()
private val numberFormatter = NumberFormat.getNumberInstance(Locale.getDefault()) private val numberFormatter = NumberFormat.getNumberInstance(Locale.getDefault())
@ -76,7 +75,7 @@ class AddonDetailsView(
} }
private fun bindLastUpdated(addon: Addon) { 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) { private fun bindVersion(addon: Addon) {
@ -132,8 +131,4 @@ class AddonDetailsView(
spannableStringBuilder.setSpan(clickable, start, end, flags) spannableStringBuilder.setSpan(clickable, start, end, flags)
spannableStringBuilder.removeSpan(link) 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.app.links.AppLinksFeature
import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.contextmenu.ContextMenuFeature import mozilla.components.feature.contextmenu.ContextMenuFeature
import mozilla.components.feature.downloads.AbstractFetchDownloadService
import mozilla.components.feature.downloads.DownloadsFeature import mozilla.components.feature.downloads.DownloadsFeature
import mozilla.components.feature.downloads.manager.FetchDownloadManager import mozilla.components.feature.downloads.manager.FetchDownloadManager
import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID 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.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.SharedViewModel import org.mozilla.fenix.home.SharedViewModel
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
@ -169,9 +167,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
customTabSessionId = arguments?.getString(EXTRA_SESSION_ID) customTabSessionId = arguments?.getString(EXTRA_SESSION_ID)
val view = if (FeatureFlags.browserChromeGestures) { val view = if (FeatureFlags.browserChromeGestures) {
inflater.inflate(R.layout.browser_gesture_wrapper, container, false).apply { inflater.inflate(R.layout.browser_gesture_wrapper, container, false)
inflater.inflate(R.layout.fragment_browser, this as SwipeGestureLayout, true)
}
} else { } else {
inflater.inflate(R.layout.fragment_browser, container, false) inflater.inflate(R.layout.fragment_browser, container, false)
} }
@ -379,8 +375,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus -> downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus ->
// If the download is just paused, don't show any in-app notification // If the download is just paused, don't show any in-app notification
if (downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.COMPLETED || if (downloadJobStatus == DownloadState.Status.COMPLETED ||
downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.FAILED downloadJobStatus == DownloadState.Status.FAILED
) { ) {
saveDownloadDialogState( saveDownloadDialogState(
@ -392,7 +388,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
val dynamicDownloadDialog = DynamicDownloadDialog( val dynamicDownloadDialog = DynamicDownloadDialog(
container = view.browserLayout, container = view.browserLayout,
downloadState = downloadState, downloadState = downloadState,
didFail = downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.FAILED, didFail = downloadJobStatus == DownloadState.Status.FAILED,
tryAgain = downloadFeature::tryAgain, tryAgain = downloadFeature::tryAgain,
onCannotOpenFile = { onCannotOpenFile = {
FenixSnackbar.make( FenixSnackbar.make(
@ -617,12 +613,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
private fun saveDownloadDialogState( private fun saveDownloadDialogState(
sessionId: String?, sessionId: String?,
downloadState: DownloadState, downloadState: DownloadState,
downloadJobStatus: AbstractFetchDownloadService.DownloadJobStatus downloadJobStatus: DownloadState.Status
) { ) {
sessionId?.let { id -> sessionId?.let { id ->
sharedViewModel.downloadDialogState[id] = Pair( sharedViewModel.downloadDialogState[id] = Pair(
downloadState, downloadState,
downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.FAILED downloadJobStatus == DownloadState.Status.FAILED
) )
} }
} }
@ -743,7 +739,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
} }
@CallSuper @CallSuper
final override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
if (findNavController().currentDestination?.id != R.id.searchFragment) { if (findNavController().currentDestination?.id != R.id.searchFragment) {
view?.hideKeyboard() view?.hideKeyboard()
@ -835,14 +831,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
sessionManager.remove(session) sessionManager.remove(session)
true true
} else { } else {
val isLastSession =
sessionManager.sessionsOfType(private = session.private).count() == 1
if (session.hasParentSession) { if (session.hasParentSession) {
sessionManager.remove(session, true) sessionManager.remove(session, true)
} }
// We want to return to home if this removed session was the last session of its type // We want to return to home if this session didn't have a parent session to select.
// and didn't have a parent session to select. val goToOverview = !session.hasParentSession
val goToOverview = isLastSession && !session.hasParentSession
!goToOverview !goToOverview
} }
} }

View File

@ -9,7 +9,6 @@ import android.animation.AnimatorListenerAdapter
import android.app.Activity import android.app.Activity
import android.graphics.PointF import android.graphics.PointF
import android.graphics.Rect import android.graphics.Rect
import android.os.Build
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewConfiguration import android.view.ViewConfiguration
@ -17,7 +16,6 @@ import androidx.annotation.Dimension
import androidx.annotation.Dimension.DP import androidx.annotation.Dimension.DP
import androidx.core.graphics.contains import androidx.core.graphics.contains
import androidx.core.graphics.toPoint import androidx.core.graphics.toPoint
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FlingAnimation import androidx.dynamicanimation.animation.FlingAnimation
@ -25,6 +23,8 @@ import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.support.ktx.android.util.dpToPx
import mozilla.components.support.ktx.android.view.getRectWithViewLocation 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.sessionsOfType
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import kotlin.math.abs 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 * Handles intercepting touch events on the toolbar for swipe gestures and executes the
* necessary animations. * necessary animations.
*/ */
@Suppress("LargeClass", "TooManyFunctions")
class ToolbarGestureHandler( class ToolbarGestureHandler(
private val activity: Activity, private val activity: Activity,
private val contentLayout: View, private val contentLayout: View,
@ -56,18 +55,6 @@ class ToolbarGestureHandler(
private val windowWidth: Int private val windowWidth: Int
get() = activity.resources.displayMetrics.widthPixels 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 previewOffset = PREVIEW_OFFSET.dpToPx(activity.resources.displayMetrics)
private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop
@ -89,7 +76,12 @@ class ToolbarGestureHandler(
GestureDirection.LEFT_TO_RIGHT 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()) preparePreview(getDestination())
true true
} else { } else {
@ -313,7 +305,7 @@ class ToolbarGestureHandler(
val toolbarLocation = toolbarLayout.getRectWithViewLocation() val toolbarLocation = toolbarLayout.getRectWithViewLocation()
// In Android 10, the system gesture touch area overlaps the bottom of the toolbar, so // In Android 10, the system gesture touch area overlaps the bottom of the toolbar, so
// lets make our swipe area taller by that amount // lets make our swipe area taller by that amount
windowInsets?.let { insets -> activity.window.decorView.getWindowInsets()?.let { insets ->
if (activity.settings().shouldUseBottomToolbar) { if (activity.settings().shouldUseBottomToolbar) {
toolbarLocation.top -= (insets.mandatorySystemGestureInsets.bottom - insets.stableInsetBottom) toolbarLocation.top -= (insets.mandatorySystemGestureInsets.bottom - insets.stableInsetBottom)
} }

View File

@ -5,32 +5,11 @@
package org.mozilla.fenix.components.metrics package org.mozilla.fenix.components.metrics
import android.content.Context import android.content.Context
import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts
import mozilla.components.browser.errorpages.ErrorType import mozilla.components.browser.errorpages.ErrorType
import mozilla.components.browser.menu.facts.BrowserMenuFacts
import mozilla.components.browser.search.SearchEngine 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.Addons
import org.mozilla.fenix.GleanMetrics.AppTheme import org.mozilla.fenix.GleanMetrics.AppTheme
import org.mozilla.fenix.GleanMetrics.Autoplay
import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.GleanMetrics.Collections
import org.mozilla.fenix.GleanMetrics.ContextMenu import org.mozilla.fenix.GleanMetrics.ContextMenu
import org.mozilla.fenix.GleanMetrics.CrashReporter 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.Events
import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.GleanMetrics.Logins
import org.mozilla.fenix.GleanMetrics.Onboarding 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.SearchShortcuts
import org.mozilla.fenix.GleanMetrics.Tip import org.mozilla.fenix.GleanMetrics.Tip
import org.mozilla.fenix.GleanMetrics.ToolbarSettings import org.mozilla.fenix.GleanMetrics.ToolbarSettings
import org.mozilla.fenix.GleanMetrics.TrackingProtection import org.mozilla.fenix.GleanMetrics.TrackingProtection
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.search.awesomebar.ShortcutsSuggestionProvider
import java.util.Locale import java.util.Locale
sealed class Event { sealed class Event {
@ -176,12 +154,19 @@ sealed class Event {
object SearchWidgetCFRCanceled : Event() object SearchWidgetCFRCanceled : Event()
object SearchWidgetCFRNotNowPressed : Event() object SearchWidgetCFRNotNowPressed : Event()
object SearchWidgetCFRAddWidgetPressed : Event() object SearchWidgetCFRAddWidgetPressed : Event()
object SearchWidgetInstalled : Event()
object OnboardingAutoSignIn : Event() object OnboardingAutoSignIn : Event()
object OnboardingManualSignIn : Event() object OnboardingManualSignIn : Event()
object OnboardingPrivacyNotice : Event() object OnboardingPrivacyNotice : Event()
object OnboardingPrivateBrowsing : Event() object OnboardingPrivateBrowsing : Event()
object OnboardingWhatsNew : Event() object OnboardingWhatsNew : Event()
object OnboardingFinish : Event() object OnboardingFinish : Event()
object ChangedToDefaultBrowser : Event()
object LoginDialogPromptDisplayed : Event()
object LoginDialogPromptCancelled : Event()
object LoginDialogPromptSave : Event()
object LoginDialogPromptNeverSave : Event()
object ContextualHintETPDisplayed : Event() object ContextualHintETPDisplayed : Event()
object ContextualHintETPDismissed : Event() object ContextualHintETPDismissed : Event()
@ -201,7 +186,21 @@ sealed class Event {
object TabsTrayShareAllTabsPressed : Event() object TabsTrayShareAllTabsPressed : Event()
object TabsTrayCloseAllTabsPressed : Event() object TabsTrayCloseAllTabsPressed : Event()
object ProgressiveWebAppOpenFromHomescreenTap : Event()
object ProgressiveWebAppInstallAsShortcut : Event()
// Interaction events with extras // 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() { data class OnboardingToolbarPosition(val position: Position) : Event() {
enum class Position { TOP, BOTTOM } enum class Position { TOP, BOTTOM }
@ -505,205 +504,19 @@ sealed class Event {
get() = mapOf(Events.tabCounterMenuActionKeys.item to item.toString().toLowerCase(Locale.ROOT)) 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 sealed class Search
internal open val extras: Map<*, String>? internal open val extras: Map<*, String>?
get() = null 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.AboutPage
import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.AppTheme import org.mozilla.fenix.GleanMetrics.AppTheme
import org.mozilla.fenix.GleanMetrics.Autoplay
import org.mozilla.fenix.GleanMetrics.BookmarksManagement import org.mozilla.fenix.GleanMetrics.BookmarksManagement
import org.mozilla.fenix.GleanMetrics.BrowserSearch import org.mozilla.fenix.GleanMetrics.BrowserSearch
import org.mozilla.fenix.GleanMetrics.Collections 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.Events
import org.mozilla.fenix.GleanMetrics.FindInPage import org.mozilla.fenix.GleanMetrics.FindInPage
import org.mozilla.fenix.GleanMetrics.History import org.mozilla.fenix.GleanMetrics.History
import org.mozilla.fenix.GleanMetrics.LoginDialog
import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.GleanMetrics.Logins
import org.mozilla.fenix.GleanMetrics.MediaNotification import org.mozilla.fenix.GleanMetrics.MediaNotification
import org.mozilla.fenix.GleanMetrics.MediaState 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.Preferences
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingMode import org.mozilla.fenix.GleanMetrics.PrivateBrowsingMode
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut
import org.mozilla.fenix.GleanMetrics.ProgressiveWebApp
import org.mozilla.fenix.GleanMetrics.QrScanner import org.mozilla.fenix.GleanMetrics.QrScanner
import org.mozilla.fenix.GleanMetrics.ReaderMode import org.mozilla.fenix.GleanMetrics.ReaderMode
import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine 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.SyncAccount
import org.mozilla.fenix.GleanMetrics.SyncAuth import org.mozilla.fenix.GleanMetrics.SyncAuth
import org.mozilla.fenix.GleanMetrics.Tab import org.mozilla.fenix.GleanMetrics.Tab
import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.GleanMetrics.Tip import org.mozilla.fenix.GleanMetrics.Tip
import org.mozilla.fenix.GleanMetrics.ToolbarSettings import org.mozilla.fenix.GleanMetrics.ToolbarSettings
import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.GleanMetrics.TopSites
@ -140,6 +144,18 @@ private val Event.wrapper: EventWrapper<*>?
{ SearchShortcuts.selected.record(it) }, { SearchShortcuts.selected.record(it) },
{ SearchShortcuts.selectedKeys.valueOf(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>( is Event.FindInPageOpened -> EventWrapper<NoExtraKeys>(
{ FindInPage.opened.record(it) } { FindInPage.opened.record(it) }
) )
@ -620,40 +636,61 @@ private val Event.wrapper: EventWrapper<*>?
) )
is Event.TabsTrayOpened -> EventWrapper<NoExtraKeys>( is Event.TabsTrayOpened -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.opened.record(it) } { TabsTray.opened.record(it) }
) )
is Event.TabsTrayClosed -> EventWrapper<NoExtraKeys>( is Event.TabsTrayClosed -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.closed.record(it) } { TabsTray.closed.record(it) }
) )
is Event.OpenedExistingTab -> EventWrapper<NoExtraKeys>( is Event.OpenedExistingTab -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.openedExistingTab.record(it) } { TabsTray.openedExistingTab.record(it) }
) )
is Event.ClosedExistingTab -> EventWrapper<NoExtraKeys>( is Event.ClosedExistingTab -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.closedExistingTab.record(it) } { TabsTray.closedExistingTab.record(it) }
) )
is Event.TabsTrayPrivateModeTapped -> EventWrapper<NoExtraKeys>( is Event.TabsTrayPrivateModeTapped -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.privateModeTapped.record(it) } { TabsTray.privateModeTapped.record(it) }
) )
is Event.TabsTrayNormalModeTapped -> EventWrapper<NoExtraKeys>( is Event.TabsTrayNormalModeTapped -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.normalModeTapped.record(it) } { TabsTray.normalModeTapped.record(it) }
) )
is Event.NewTabTapped -> EventWrapper<NoExtraKeys>( is Event.NewTabTapped -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.newTabTapped.record(it) } { TabsTray.newTabTapped.record(it) }
) )
is Event.NewPrivateTabTapped -> EventWrapper<NoExtraKeys>( is Event.NewPrivateTabTapped -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.newPrivateTabTapped.record(it) } { TabsTray.newPrivateTabTapped.record(it) }
) )
is Event.TabsTrayMenuOpened -> EventWrapper<NoExtraKeys>( is Event.TabsTrayMenuOpened -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.menuOpened.record(it) } { TabsTray.menuOpened.record(it) }
) )
is Event.TabsTraySaveToCollectionPressed -> EventWrapper<NoExtraKeys>( is Event.TabsTraySaveToCollectionPressed -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.saveToCollection.record(it) } { TabsTray.saveToCollection.record(it) }
) )
is Event.TabsTrayShareAllTabsPressed -> EventWrapper<NoExtraKeys>( is Event.TabsTrayShareAllTabsPressed -> EventWrapper<NoExtraKeys>(
{ org.mozilla.fenix.GleanMetrics.TabsTray.shareAllTabs.record(it) } { TabsTray.shareAllTabs.record(it) }
) )
is Event.TabsTrayCloseAllTabsPressed -> EventWrapper<NoExtraKeys>( 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: // Don't record other events in Glean:
@ -665,6 +702,8 @@ private val Event.wrapper: EventWrapper<*>?
is Event.DismissedOnboarding -> null is Event.DismissedOnboarding -> null
is Event.FennecToFenixMigrated -> null is Event.FennecToFenixMigrated -> null
is Event.AddonInstalled -> null is Event.AddonInstalled -> null
is Event.SearchWidgetInstalled -> null
is Event.ChangedToDefaultBrowser -> null
} }
class GleanMetricsService(private val context: Context) : MetricsService { 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.components.metrics.MozillaProductDetector.MozillaProducts
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import java.util.Locale import java.util.Locale
import java.util.MissingResourceException
import java.util.UUID.randomUUID import java.util.UUID.randomUUID
private val Event.name: String? private val Event.name: String?
@ -39,6 +40,9 @@ private val Event.name: String?
is Event.DismissedOnboarding -> "E_Dismissed_Onboarding" is Event.DismissedOnboarding -> "E_Dismissed_Onboarding"
is Event.FennecToFenixMigrated -> "E_Fennec_To_Fenix_Migrated" is Event.FennecToFenixMigrated -> "E_Fennec_To_Fenix_Migrated"
is Event.AddonInstalled -> "E_Addon_Installed" 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 // Do not track other events in Leanplum
else -> null else -> null
@ -80,12 +84,19 @@ class LeanplumMetricsService(private val application: Application) : MetricsServ
leanplumJob = scope.launch { leanplumJob = scope.launch {
val applicationSetLocale = LocaleManager.getCurrentLocale(application) val applicationSetLocale = LocaleManager.getCurrentLocale(application)
val currentLocale = when (applicationSetLocale != null) { val currentLocale = applicationSetLocale ?: Locale.getDefault()
true -> applicationSetLocale.isO3Language val languageCode =
false -> Locale.getDefault().isO3Language currentLocale.iso3LanguageOrNull
} ?: currentLocale.language.let {
if (!isLeanplumEnabled(currentLocale)) { if (it.isNotBlank()) {
Log.i(LOGTAG, "Leanplum is not available for this locale: $currentLocale") it
} else {
currentLocale.toString()
}
}
if (!isLeanplumEnabled(languageCode)) {
Log.i(LOGTAG, "Leanplum is not available for this locale: $languageCode")
return@launch return@launch
} }
@ -167,6 +178,12 @@ class LeanplumMetricsService(private val application: Application) : MetricsServ
return LEANPLUM_ENABLED_LOCALES.contains(locale) return LEANPLUM_ENABLED_LOCALES.contains(locale)
} }
private val Locale.iso3LanguageOrNull: String?
get() =
try {
this.isO3Language
} catch (_: MissingResourceException) { null }
companion object { companion object {
private const val LOGTAG = "LeanplumMetricsService" private const val LOGTAG = "LeanplumMetricsService"
@ -178,7 +195,7 @@ class LeanplumMetricsService(private val application: Application) : MetricsServ
get() = BuildConfig.LEANPLUM_TOKEN.orEmpty() get() = BuildConfig.LEANPLUM_TOKEN.orEmpty()
// Leanplum needs to be enabled for the following locales. // Leanplum needs to be enabled for the following locales.
// Irrespective of the actual device location. // Irrespective of the actual device location.
private val LEANPLUM_ENABLED_LOCALES = listOf( private val LEANPLUM_ENABLED_LOCALES = setOf(
"eng", // English "eng", // English
"zho", // Chinese "zho", // Chinese
"deu", // German "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.Context
import android.content.Intent import android.content.Intent
import android.os.SystemClock
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.navigation.fragment.navArgs 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.BaseBrowserFragment
import org.mozilla.fenix.browser.CustomTabContextMenuCandidate import org.mozilla.fenix.browser.CustomTabContextMenuCandidate
import org.mozilla.fenix.browser.FenixSnackbarDelegate import org.mozilla.fenix.browser.FenixSnackbarDelegate
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings 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 { override fun removeSessionIfNeeded(): Boolean {
return customTabsIntegration.onBackPressed() || super.removeSessionIfNeeded() return customTabsIntegration.onBackPressed() || super.removeSessionIfNeeded()
} }
@ -192,4 +211,9 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
view, view,
FenixSnackbarDelegate(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 * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, 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.feature.logins.exceptions.LoginException
import mozilla.components.lib.state.Action import mozilla.components.lib.state.Action
@ -26,7 +26,7 @@ sealed class ExceptionsFragmentAction : Action {
* The state for the Exceptions Screen * The state for the Exceptions Screen
* @property items List of exceptions to display * @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. * 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 * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, 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.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -13,10 +13,9 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe import androidx.lifecycle.observe
import kotlinx.android.synthetic.main.fragment_exceptions.view.* import kotlinx.android.synthetic.main.fragment_exceptions.view.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch import kotlinx.coroutines.plus
import mozilla.components.feature.logins.exceptions.LoginException
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.StoreProvider
@ -45,14 +44,17 @@ class LoginExceptionsFragment : Fragment() {
val view = inflater.inflate(R.layout.fragment_exceptions, container, false) val view = inflater.inflate(R.layout.fragment_exceptions, container, false)
exceptionsStore = StoreProvider.get(this) { exceptionsStore = StoreProvider.get(this) {
ExceptionsFragmentStore( ExceptionsFragmentStore(
ExceptionsFragmentState( ExceptionsFragmentState(items = emptyList())
items = listOf()
)
) )
} }
exceptionsInteractor = exceptionsInteractor = DefaultLoginExceptionsInteractor(
LoginExceptionsInteractor(::deleteOneItem, ::deleteAllItems) ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO,
exceptionsView = LoginExceptionsView(view.exceptionsLayout, exceptionsInteractor) loginExceptionStorage = requireComponents.core.loginExceptionStorage
)
exceptionsView = LoginExceptionsView(
view.exceptionsLayout,
exceptionsInteractor
)
subscribeToLoginExceptions() subscribeToLoginExceptions()
return view return view
} }
@ -67,19 +69,7 @@ class LoginExceptionsFragment : Fragment() {
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
consumeFrom(exceptionsStore) { consumeFrom(exceptionsStore) {
exceptionsView.update(it) exceptionsView.update(it.items)
}
}
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)
} }
} }
} }

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 * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, 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.concept.engine.content.blocking.TrackingProtectionException
import mozilla.components.lib.state.Action import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store 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. * The [Store] for holding the [ExceptionsFragmentState] and applying [ExceptionsFragmentAction]s.
*/ */
@ -32,7 +26,7 @@ sealed class ExceptionsFragmentAction : Action {
* The state for the Exceptions Screen * The state for the Exceptions Screen
* @property items List of exceptions to display * @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. * 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 * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, 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 android.view.View
import androidx.recyclerview.widget.RecyclerView 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.R
import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsInteractor import org.mozilla.fenix.exceptions.ExceptionsInteractor
class ExceptionsDeleteButtonViewHolder( class ExceptionsDeleteButtonViewHolder(
view: View, view: View,
private val interactor: ExceptionsInteractor private val interactor: ExceptionsInteractor<*>
) : RecyclerView.ViewHolder(view) { ) : RecyclerView.ViewHolder(view) {
private val deleteButton = view.removeAllExceptions
init { init {
val deleteButton: MaterialButton = view.findViewById(R.id.removeAllExceptions)
deleteButton.setOnClickListener { deleteButton.setOnClickListener {
interactor.onDeleteAll() 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 * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.loginexceptions.viewholders package org.mozilla.fenix.exceptions.viewholders
import android.view.View import android.view.View
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.exceptions_description.view.* import kotlinx.android.synthetic.main.exceptions_description.view.*
import org.mozilla.fenix.R import org.mozilla.fenix.R
class LoginExceptionsHeaderViewHolder( class ExceptionsHeaderViewHolder(
view: View view: View,
@StringRes description: Int
) : RecyclerView.ViewHolder(view) { ) : RecyclerView.ViewHolder(view) {
init { init {
view.exceptions_description.text = view.exceptions_description.text = view.context.getString(description)
view.context.getString(R.string.preferences_passwords_exceptions_description)
} }
companion object { 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 package org.mozilla.fenix.ext
import android.graphics.Rect import android.graphics.Rect
import android.os.Build
import android.view.TouchDelegate import android.view.TouchDelegate
import android.view.View 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 import mozilla.components.support.ktx.android.util.dpToPx
fun View.increaseTapArea(extraDps: Int) { fun View.increaseTapArea(extraDps: Int) {
@ -26,3 +30,61 @@ fun View.removeTouchDelegate() {
parent.touchDelegate = null 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, hideOnboarding = ::hideOnboardingAndOpenSearch,
registerCollectionStorageObserver = ::registerCollectionStorageObserver, registerCollectionStorageObserver = ::registerCollectionStorageObserver,
showDeleteCollectionPrompt = ::showDeleteCollectionPrompt, showDeleteCollectionPrompt = ::showDeleteCollectionPrompt,
showTabTray = ::openTabTray showTabTray = ::openTabTray,
handleSwipedItemDeletionCancel = ::handleSwipedItemDeletionCancel
) )
) )
updateLayout(view) 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 val context = context ?: return
AlertDialog.Builder(context).apply { AlertDialog.Builder(context).apply {
setTitle(title) setTitle(title)
setMessage(message) setMessage(message)
setNegativeButton(R.string.tab_collection_dialog_negative) { dialog: DialogInterface, _ -> setNegativeButton(R.string.tab_collection_dialog_negative) { dialog: DialogInterface, _ ->
if (wasSwiped) {
handleSwipedItemDeletionCancel()
}
dialog.cancel() dialog.cancel()
} }
setPositiveButton(R.string.tab_collection_dialog_positive) { dialog: DialogInterface, _ -> 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 view?.add_tabs_to_collections_button?.isVisible = tabCount > 0
} }
private fun handleSwipedItemDeletionCancel() {
view?.sessionControlRecyclerView?.adapter?.notifyDataSetChanged()
}
companion object { companion object {
const val ALL_NORMAL_TABS = "all_normal" const val ALL_NORMAL_TABS = "all_normal"
const val ALL_PRIVATE_TABS = "all_private" 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 mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.home.OnboardingState 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.CollectionHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder 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) { sealed class AdapterItem(@LayoutRes val viewType: Int) {
data class TipItem(val tip: Tip) : AdapterItem( data class TipItem(val tip: Tip) : AdapterItem(
ButtonTipViewHolder.LAYOUT_ID) ButtonTipViewHolder.LAYOUT_ID
data class TopSiteList(val topSites: List<TopSite>) : AdapterItem(TopSiteViewHolder.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 PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID)
object NoCollectionsMessage : AdapterItem(NoCollectionsMessageViewHolder.LAYOUT_ID) object NoCollectionsMessage : AdapterItem(NoCollectionsMessageViewHolder.LAYOUT_ID)
@ -48,32 +63,48 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
val collection: TabCollection, val collection: TabCollection,
val expanded: Boolean val expanded: Boolean
) : AdapterItem(CollectionViewHolder.LAYOUT_ID) { ) : 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( data class TabInCollectionItem(
val collection: TabCollection, val collection: TabCollection,
val tab: ComponentTab, val tab: ComponentTab,
val isLastTab: Boolean val isLastTab: Boolean
) : AdapterItem(TabInCollectionViewHolder.LAYOUT_ID) { ) : 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) object OnboardingHeader : AdapterItem(OnboardingHeaderViewHolder.LAYOUT_ID)
data class OnboardingSectionHeader( data class OnboardingSectionHeader(
val labelBuilder: (Context) -> String val labelBuilder: (Context) -> String
) : AdapterItem(OnboardingSectionHeaderViewHolder.LAYOUT_ID) { ) : 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) object OnboardingManualSignIn : AdapterItem(OnboardingManualSignInViewHolder.LAYOUT_ID)
data class OnboardingAutomaticSignIn( data class OnboardingAutomaticSignIn(
val state: OnboardingState.SignedOutCanAutoSignIn val state: OnboardingState.SignedOutCanAutoSignIn
) : AdapterItem(OnboardingAutomaticSignInViewHolder.LAYOUT_ID) ) : AdapterItem(OnboardingAutomaticSignInViewHolder.LAYOUT_ID)
object OnboardingThemePicker : AdapterItem(OnboardingThemePickerViewHolder.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 OnboardingPrivateBrowsing : AdapterItem(OnboardingPrivateBrowsingViewHolder.LAYOUT_ID)
object OnboardingPrivacyNotice : AdapterItem(OnboardingPrivacyNoticeViewHolder.LAYOUT_ID) object OnboardingPrivacyNotice : AdapterItem(OnboardingPrivacyNoticeViewHolder.LAYOUT_ID)
object OnboardingFinish : AdapterItem(OnboardingFinishViewHolder.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) 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 * Returns a payload if there's been a change, or null if not
*/ */
open fun getChangePayload(newItem: AdapterItem): Any? = null open fun getChangePayload(newItem: AdapterItem): Any? = null
open fun contentsSameAs(other: AdapterItem) = this::class == other::class
} }
class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() { 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") @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? { override fun getChangePayload(oldItem: AdapterItem, newItem: AdapterItem): Any? {
return oldItem.getChangePayload(newItem) ?: return super.getChangePayload(oldItem, newItem) 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( class SessionControlAdapter(
@ -119,23 +145,42 @@ class SessionControlAdapter(
return when (viewType) { return when (viewType) {
ButtonTipViewHolder.LAYOUT_ID -> ButtonTipViewHolder(view, interactor) ButtonTipViewHolder.LAYOUT_ID -> ButtonTipViewHolder(view, interactor)
TopSiteViewHolder.LAYOUT_ID -> TopSiteViewHolder(view, interactor) TopSiteViewHolder.LAYOUT_ID -> TopSiteViewHolder(view, interactor)
PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(view, interactor) PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(
view,
interactor
)
NoCollectionsMessageViewHolder.LAYOUT_ID -> NoCollectionsMessageViewHolder.LAYOUT_ID ->
NoCollectionsMessageViewHolder(view, interactor, hasNormalTabsOpened) NoCollectionsMessageViewHolder(view, interactor, hasNormalTabsOpened)
CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view) CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view)
CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, interactor) 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) OnboardingHeaderViewHolder.LAYOUT_ID -> OnboardingHeaderViewHolder(view)
OnboardingSectionHeaderViewHolder.LAYOUT_ID -> OnboardingSectionHeaderViewHolder(view) OnboardingSectionHeaderViewHolder.LAYOUT_ID -> OnboardingSectionHeaderViewHolder(view)
OnboardingAutomaticSignInViewHolder.LAYOUT_ID -> OnboardingAutomaticSignInViewHolder(view) OnboardingAutomaticSignInViewHolder.LAYOUT_ID -> OnboardingAutomaticSignInViewHolder(
view
)
OnboardingManualSignInViewHolder.LAYOUT_ID -> OnboardingManualSignInViewHolder(view) OnboardingManualSignInViewHolder.LAYOUT_ID -> OnboardingManualSignInViewHolder(view)
OnboardingThemePickerViewHolder.LAYOUT_ID -> OnboardingThemePickerViewHolder(view) OnboardingThemePickerViewHolder.LAYOUT_ID -> OnboardingThemePickerViewHolder(view)
OnboardingTrackingProtectionViewHolder.LAYOUT_ID -> OnboardingTrackingProtectionViewHolder(view) OnboardingTrackingProtectionViewHolder.LAYOUT_ID -> OnboardingTrackingProtectionViewHolder(
OnboardingPrivateBrowsingViewHolder.LAYOUT_ID -> OnboardingPrivateBrowsingViewHolder(view, interactor) view
OnboardingPrivacyNoticeViewHolder.LAYOUT_ID -> OnboardingPrivacyNoticeViewHolder(view, interactor) )
OnboardingPrivateBrowsingViewHolder.LAYOUT_ID -> OnboardingPrivateBrowsingViewHolder(
view,
interactor
)
OnboardingPrivacyNoticeViewHolder.LAYOUT_ID -> OnboardingPrivacyNoticeViewHolder(
view,
interactor
)
OnboardingFinishViewHolder.LAYOUT_ID -> OnboardingFinishViewHolder(view, interactor) OnboardingFinishViewHolder.LAYOUT_ID -> OnboardingFinishViewHolder(view, interactor)
OnboardingWhatsNewViewHolder.LAYOUT_ID -> OnboardingWhatsNewViewHolder(view, interactor) OnboardingWhatsNewViewHolder.LAYOUT_ID -> OnboardingWhatsNewViewHolder(view, interactor)
OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder(view) OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder(
view
)
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }

View File

@ -63,7 +63,7 @@ interface SessionControlController {
/** /**
* @see [CollectionInteractor.onCollectionRemoveTab] * @see [CollectionInteractor.onCollectionRemoveTab]
*/ */
fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab) fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab, wasSwiped: Boolean)
/** /**
* @see [CollectionInteractor.onCollectionShareTabsClicked] * @see [CollectionInteractor.onCollectionShareTabsClicked]
@ -160,8 +160,15 @@ class DefaultSessionControlController(
private val viewLifecycleScope: CoroutineScope, private val viewLifecycleScope: CoroutineScope,
private val hideOnboarding: () -> Unit, private val hideOnboarding: () -> Unit,
private val registerCollectionStorageObserver: () -> Unit, private val registerCollectionStorageObserver: () -> Unit,
private val showDeleteCollectionPrompt: (tabCollection: TabCollection, title: String?, message: String) -> Unit, private val showDeleteCollectionPrompt: (
private val showTabTray: () -> Unit tabCollection: TabCollection,
title: String?,
message: String,
wasSwiped: Boolean,
handleSwipedItemDeletionCancel: () -> Unit
) -> Unit,
private val showTabTray: () -> Unit,
private val handleSwipedItemDeletionCancel: () -> Unit
) : SessionControlController { ) : SessionControlController {
override fun handleCollectionAddTabTapped(collection: TabCollection) { override fun handleCollectionAddTabTapped(collection: TabCollection) {
@ -206,7 +213,7 @@ class DefaultSessionControlController(
metrics.track(Event.CollectionAllTabsRestored) metrics.track(Event.CollectionAllTabsRestored)
} }
override fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab) { override fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab, wasSwiped: Boolean) {
metrics.track(Event.CollectionTabRemoved) metrics.track(Event.CollectionTabRemoved)
if (collection.tabs.size == 1) { if (collection.tabs.size == 1) {
@ -216,7 +223,7 @@ class DefaultSessionControlController(
) )
val message = val message =
activity.resources.getString(R.string.delete_tab_and_collection_dialog_message) activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
showDeleteCollectionPrompt(collection, title, message) showDeleteCollectionPrompt(collection, title, message, wasSwiped, handleSwipedItemDeletionCancel)
} else { } else {
viewLifecycleScope.launch(Dispatchers.IO) { viewLifecycleScope.launch(Dispatchers.IO) {
tabCollectionStorage.removeTabFromCollection(collection, tab) tabCollectionStorage.removeTabFromCollection(collection, tab)
@ -232,7 +239,7 @@ class DefaultSessionControlController(
override fun handleDeleteCollectionTapped(collection: TabCollection) { override fun handleDeleteCollectionTapped(collection: TabCollection) {
val message = val message =
activity.resources.getString(R.string.tab_collection_dialog_message, collection.title) 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) { override fun handleOpenInPrivateTabClicked(topSite: TopSite) {

View File

@ -54,7 +54,7 @@ interface CollectionInteractor {
* @param collection The collection of tabs that will be modified. * @param collection The collection of tabs that will be modified.
* @param tab The tab to remove from the tab collection. * @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 * 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) controller.handleCollectionOpenTabsTapped(collection)
} }
override fun onCollectionRemoveTab(collection: TabCollection, tab: Tab) { override fun onCollectionRemoveTab(collection: TabCollection, tab: Tab, wasSwiped: Boolean) {
controller.handleCollectionRemoveTab(collection, tab) controller.handleCollectionRemoveTab(collection, tab, wasSwiped)
} }
override fun onCollectionShareTabsClicked(collection: TabCollection) { override fun onCollectionShareTabsClicked(collection: TabCollection) {

View File

@ -29,7 +29,7 @@ class SwipeToDeleteCallback(
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
when (viewHolder) { when (viewHolder) {
is TabInCollectionViewHolder -> { 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.increaseTapArea(buttonIncreaseDps)
list_item_action_button.setOnClickListener { 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 android.widget.PopupWindow
import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.appcompat.content.res.AppCompatResources.getDrawable
import kotlinx.android.synthetic.main.top_site_item.* 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.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
@ -44,7 +43,7 @@ class TopSiteItemViewHolder(
} }
top_site_item.setOnLongClickListener { 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 -> it.setOnTouchListener @SuppressLint("ClickableViewAccessibility") { v, event ->
onTouchEvent(v, event, menu) onTouchEvent(v, event, menu)
} }

View File

@ -67,6 +67,8 @@ class LibrarySiteItemView @JvmOverloads constructor(
val overflowView: ImageButton get() = overflow_menu val overflowView: ImageButton get() = overflow_menu
private var iconUrl: String? = null
init { init {
LayoutInflater.from(context).inflate(R.layout.library_site_item, this, true) LayoutInflater.from(context).inflate(R.layout.library_site_item, this, true)
@ -94,6 +96,9 @@ class LibrarySiteItemView @JvmOverloads constructor(
} }
fun loadFavicon(url: String) { fun loadFavicon(url: String) {
if (iconUrl == url) return
iconUrl = url
context.components.core.icons.loadIntoView(favicon, url) context.components.core.icons.loadIntoView(favicon, url)
} }

View File

@ -68,10 +68,10 @@ class HistoryListItemViewHolder(
itemView.history_layout.loadFavicon(item.url) itemView.history_layout.loadFavicon(item.url)
} }
if (item !in selectionHolder.selectedItems) { if (mode is HistoryFragmentState.Mode.Editing) {
itemView.overflow_menu.showAndEnable()
} else {
itemView.overflow_menu.hideAndDisable() itemView.overflow_menu.hideAndDisable()
} else {
itemView.overflow_menu.showAndEnable()
} }
this.item = item 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) updateSuggestionProvidersVisibility(state)
// Do not make suggestions based on user's current URL unless it's a search shortcut // 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 return
} }

View File

@ -25,6 +25,7 @@ import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile import mozilla.components.concept.sync.Profile
import mozilla.components.support.ktx.android.content.hasCamera
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -188,7 +189,16 @@ class SettingsFragment : PreferenceFragmentCompat() {
val directions: NavDirections? = when (preference.key) { val directions: NavDirections? = when (preference.key) {
resources.getString(R.string.pref_key_sign_in) -> { 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) -> { resources.getString(R.string.pref_key_search_settings) -> {
SettingsFragmentDirections.actionSettingsFragmentToSearchEngineFragment() SettingsFragmentDirections.actionSettingsFragmentToSearchEngineFragment()

View File

@ -17,6 +17,7 @@ import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
import kotlinx.android.synthetic.main.fragment_about.* import kotlinx.android.synthetic.main.fragment_about.*
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
@ -38,6 +39,7 @@ import org.mozilla.geckoview.BuildConfig as GeckoViewBuildConfig
*/ */
class AboutFragment : Fragment(), AboutPageListener { class AboutFragment : Fragment(), AboutPageListener {
private lateinit var headerAppName: String
private lateinit var appName: String private lateinit var appName: String
private val aboutPageAdapter: AboutPageAdapter = AboutPageAdapter(this) private val aboutPageAdapter: AboutPageAdapter = AboutPageAdapter(this)
@ -48,6 +50,8 @@ class AboutFragment : Fragment(), AboutPageListener {
): View? { ): View? {
val rootView = inflater.inflate(R.layout.fragment_about, container, false) val rootView = inflater.inflate(R.layout.fragment_about, container, false)
appName = getString(R.string.app_name) 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) activity?.title = getString(R.string.preferences_about, appName)
return rootView return rootView
@ -64,10 +68,12 @@ class AboutFragment : Fragment(), AboutPageListener {
) )
} }
lifecycle.addObserver(SecretDebugMenuTrigger( lifecycle.addObserver(
logoView = wordmark, SecretDebugMenuTrigger(
settings = view.context.settings() logoView = wordmark,
)) settings = view.context.settings()
)
)
populateAboutHeader() populateAboutHeader()
aboutPageAdapter.submitList(populateAboutList()) aboutPageAdapter.submitList(populateAboutList())
@ -75,12 +81,15 @@ class AboutFragment : Fragment(), AboutPageListener {
private fun populateAboutHeader() { private fun populateAboutHeader() {
val aboutText = try { 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 versionCode = PackageInfoCompat.getLongVersionCode(packageInfo).toString()
val componentsAbbreviation = getString(R.string.components_abbreviation) 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 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 appServicesAbbreviation = getString(R.string.app_services_abbreviation)
val appServicesVersion = mozilla.components.Build.applicationServicesVersion 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 val buildDate = BuildConfig.BUILD_DATE
about_text.text = aboutText about_text.text = aboutText
@ -160,7 +169,12 @@ class AboutFragment : Fragment(), AboutPageListener {
private fun openLibrariesPage() { private fun openLibrariesPage() {
startActivity(Intent(context, OssLicensesMenuActivity::class.java)) 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) { override fun onAboutItemClicked(item: AboutItem) {

View File

@ -13,6 +13,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
/** /**
@ -21,7 +22,13 @@ import org.mozilla.fenix.ext.settings
fun deleteAndQuit(activity: Activity, coroutineScope: CoroutineScope, snackbar: FenixSnackbar?) { fun deleteAndQuit(activity: Activity, coroutineScope: CoroutineScope, snackbar: FenixSnackbar?) {
coroutineScope.launch { coroutineScope.launch {
val settings = activity.settings() 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 { snackbar?.apply {
setText(activity.getString(R.string.deleting_browsing_data_in_progress)) setText(activity.getString(R.string.deleting_browsing_data_in_progress))

View File

@ -4,11 +4,12 @@
package org.mozilla.fenix.settings.deletebrowsingdata package org.mozilla.fenix.settings.deletebrowsingdata
import android.content.Context
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.components.concept.engine.Engine 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 import kotlin.coroutines.CoroutineContext
interface DeleteBrowsingDataController { interface DeleteBrowsingDataController {
@ -21,13 +22,16 @@ interface DeleteBrowsingDataController {
} }
class DefaultDeleteBrowsingDataController( class DefaultDeleteBrowsingDataController(
val context: Context, private val removeAllTabs: TabsUseCases.RemoveAllTabsUseCase,
val coroutineContext: CoroutineContext = Dispatchers.Main private val historyStorage: HistoryStorage,
private val permissionStorage: PermissionStorage,
private val engine: Engine,
private val coroutineContext: CoroutineContext = Dispatchers.Main
) : DeleteBrowsingDataController { ) : DeleteBrowsingDataController {
override suspend fun deleteTabs() { override suspend fun deleteTabs() {
withContext(coroutineContext) { withContext(coroutineContext) {
context.components.useCases.tabsUseCases.removeAllTabs.invoke() removeAllTabs.invoke()
} }
} }
@ -37,14 +41,14 @@ class DefaultDeleteBrowsingDataController(
override suspend fun deleteHistoryAndDOMStorages() { override suspend fun deleteHistoryAndDOMStorages() {
withContext(coroutineContext) { 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() { override suspend fun deleteCookies() {
withContext(coroutineContext) { withContext(coroutineContext) {
context.components.core.engine.clearData( engine.clearData(
Engine.BrowsingData.select( Engine.BrowsingData.select(
Engine.BrowsingData.COOKIES, Engine.BrowsingData.COOKIES,
Engine.BrowsingData.AUTH_SESSIONS Engine.BrowsingData.AUTH_SESSIONS
@ -55,7 +59,7 @@ class DefaultDeleteBrowsingDataController(
override suspend fun deleteCachedFiles() { override suspend fun deleteCachedFiles() {
withContext(coroutineContext) { withContext(coroutineContext) {
context.components.core.engine.clearData( engine.clearData(
Engine.BrowsingData.select(Engine.BrowsingData.ALL_CACHES) Engine.BrowsingData.select(Engine.BrowsingData.ALL_CACHES)
) )
} }
@ -63,10 +67,10 @@ class DefaultDeleteBrowsingDataController(
override suspend fun deleteSitePermissions() { override suspend fun deleteSitePermissions() {
withContext(coroutineContext) { withContext(coroutineContext) {
context.components.core.engine.clearData( engine.clearData(
Engine.BrowsingData.select(Engine.BrowsingData.ALL_SITE_SETTINGS) 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.R
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) 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() settings = requireContext().settings()
getCheckboxes().forEach { getCheckboxes().forEach {

View File

@ -5,51 +5,68 @@
package org.mozilla.fenix.settings.logins package org.mozilla.fenix.settings.logins
import android.content.Context import android.content.Context
import mozilla.components.browser.menu.BrowserMenuBuilder import androidx.annotation.VisibleForTesting
import mozilla.components.browser.menu.item.SimpleBrowserMenuHighlightableItem 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 mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.R 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( class SavedLoginsSortingStrategyMenu(
private val context: Context, private val context: Context,
private val itemToHighlight: Item, private val savedLoginsInteractor: SavedLoginsInteractor
private val onItemTapped: (Item) -> Unit = {}
) { ) {
sealed class Item { enum class Item(val strategyString: String) {
object AlphabeticallySort : Item() AlphabeticallySort("ALPHABETICALLY"),
object LastUsedSort : Item() 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 { @VisibleForTesting
listOfNotNull( internal fun menuItems(itemToHighlight: Item): List<TextMenuCandidate> {
SimpleBrowserMenuHighlightableItem( val textStyle = TextStyle(
label = context.getString(R.string.saved_logins_sort_strategy_alphabetically), color = context.getColorFromAttr(R.attr.primaryText)
textColorResource = ThemeManager.resolveAttribute(R.attr.primaryText, context), )
itemType = Item.AlphabeticallySort,
backgroundTint = context.getColorFromAttr(R.attr.colorControlHighlight), val highlight = HighPriorityHighlightEffect(
isHighlighted = { itemToHighlight == Item.AlphabeticallySort } 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)
)
}, },
TextMenuCandidate(
SimpleBrowserMenuHighlightableItem( text = context.getString(R.string.saved_logins_sort_strategy_last_used),
label = context.getString(R.string.saved_logins_sort_strategy_last_used), textStyle = textStyle,
textColorResource = ThemeManager.resolveAttribute(R.attr.primaryText, context), effect = if (itemToHighlight == Item.LastUsedSort) highlight else null
itemType = Item.LastUsedSort,
backgroundTint = context.getColorFromAttr(R.attr.colorControlHighlight),
isHighlighted = { itemToHighlight == Item.LastUsedSort }
) { ) {
onItemTapped.invoke(Item.LastUsedSort) savedLoginsInteractor.onSortingStrategyChanged(
SortingStrategy.LastUsed
)
} }
) )
} }
internal fun updateMenu(itemToHighlight: Item) { fun updateMenu(itemToHighlight: Item) {
menuItems.forEach { menuController.submitList(menuItems(itemToHighlight))
it.isHighlighted = { itemToHighlight == it.itemType }
}
} }
} }

View File

@ -4,16 +4,21 @@
package org.mozilla.fenix.settings.logins package org.mozilla.fenix.settings.logins
import android.content.Context
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.preference.Preference import androidx.preference.Preference
import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature
import mozilla.components.service.fxa.SyncEngine import mozilla.components.service.fxa.SyncEngine
import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.service.fxa.manager.SyncEnginesStorage import mozilla.components.service.fxa.manager.SyncEnginesStorage
import mozilla.components.support.ktx.android.content.hasCamera
import org.mozilla.fenix.R 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 import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections
/** /**
@ -23,7 +28,9 @@ class SyncLoginsPreferenceView(
private val syncLoginsPreference: Preference, private val syncLoginsPreference: Preference,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
accountManager: FxaAccountManager, accountManager: FxaAccountManager,
private val navController: NavController private val navController: NavController,
private val accountsAuthFeature: FirefoxAccountsAuthFeature,
private val metrics: MetricController
) { ) {
init { init {
@ -68,7 +75,15 @@ class SyncLoginsPreferenceView(
syncLoginsPreference.apply { syncLoginsPreference.apply {
summary = context.getString(R.string.preferences_passwords_sync_logins_sign_in) summary = context.getString(R.string.preferences_passwords_sync_logins_sign_in)
setOnPreferenceClickListener { 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 true
} }
} }
@ -102,4 +117,9 @@ class SyncLoginsPreferenceView(
val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToTurnOnSyncFragment() val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToTurnOnSyncFragment()
navController.navigate(directions) 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 package org.mozilla.fenix.settings.logins.controller
import android.content.Context
import android.util.Log import android.util.Log
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers 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.InvalidRecordException
import mozilla.components.service.sync.logins.LoginsStorageException import mozilla.components.service.sync.logins.LoginsStorageException
import mozilla.components.service.sync.logins.NoSuchRecordException import mozilla.components.service.sync.logins.NoSuchRecordException
import mozilla.components.service.sync.logins.SyncableLoginsStorage
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.settings.logins.LoginsAction import org.mozilla.fenix.settings.logins.LoginsAction
import org.mozilla.fenix.settings.logins.LoginsFragmentStore import org.mozilla.fenix.settings.logins.LoginsFragmentStore
import org.mozilla.fenix.settings.logins.fragment.EditLoginFragmentDirections 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 * Controller for all saved logins interactions with the password storage component
*/ */
open class SavedLoginsStorageController( open class SavedLoginsStorageController(
private val context: Context, private val passwordsStorage: SyncableLoginsStorage,
private val viewLifecycleScope: CoroutineScope, private val viewLifecycleScope: CoroutineScope,
private val navController: NavController, 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? = private suspend fun getLogin(loginId: String): Login? = passwordsStorage.get(loginId)
context.components.core.passwordsStorage.get(loginId)
fun delete(loginId: String) { fun delete(loginId: String) {
var deleteLoginJob: Deferred<Boolean>? = null var deleteLoginJob: Deferred<Boolean>? = null
val deleteJob = viewLifecycleScope.launch(Dispatchers.IO) { val deleteJob = viewLifecycleScope.launch(ioDispatcher) {
deleteLoginJob = async { deleteLoginJob = async {
context.components.core.passwordsStorage.delete(loginId) passwordsStorage.delete(loginId)
} }
deleteLoginJob?.await() deleteLoginJob?.await()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -58,10 +58,10 @@ open class SavedLoginsStorageController(
fun save(loginId: String, usernameText: String, passwordText: String) { fun save(loginId: String, usernameText: String, passwordText: String) {
var saveLoginJob: Deferred<Unit>? = null var saveLoginJob: Deferred<Unit>? = null
viewLifecycleScope.launch(Dispatchers.IO) { viewLifecycleScope.launch(ioDispatcher) {
saveLoginJob = async { saveLoginJob = async {
// must retrieve from storage to get the httpsRealm and formActionOrigin // 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 // Update requires a Login type, which needs at least one of
// httpRealm or formActionOrigin // httpRealm or formActionOrigin
@ -95,16 +95,20 @@ open class SavedLoginsStorageController(
private suspend fun save(loginToSave: Login) { private suspend fun save(loginToSave: Login) {
try { try {
context.components.core.passwordsStorage.update(loginToSave) passwordsStorage.update(loginToSave)
} catch (loginException: LoginsStorageException) { } catch (loginException: LoginsStorageException) {
when (loginException) { when (loginException) {
is NoSuchRecordException, is NoSuchRecordException,
is InvalidRecordException -> { is InvalidRecordException -> {
Log.e("Edit login", Log.e(
"Failed to save edited login.", loginException) "Edit login",
"Failed to save edited login.", loginException
)
} }
else -> Log.e("Edit login", else -> Log.e(
"Failed to save edited login.", loginException) "Edit login",
"Failed to save edited login.", loginException
)
} }
} }
} }
@ -121,10 +125,10 @@ open class SavedLoginsStorageController(
fun findPotentialDuplicates(loginId: String) { fun findPotentialDuplicates(loginId: String) {
var deferredLogin: Deferred<List<Login>>? = null var deferredLogin: Deferred<List<Login>>? = null
// What scope should be used here? // What scope should be used here?
val fetchLoginJob = viewLifecycleScope.launch(Dispatchers.IO) { val fetchLoginJob = viewLifecycleScope.launch(ioDispatcher) {
deferredLogin = async { deferredLogin = async {
val login = getLogin(loginId) val login = getLogin(loginId)
context.components.core.passwordsStorage.getPotentialDupesIgnoringUsername(login!!) passwordsStorage.getPotentialDupesIgnoringUsername(login!!)
} }
val fetchedDuplicatesList = deferredLogin?.await() val fetchedDuplicatesList = deferredLogin?.await()
fetchedDuplicatesList?.let { list -> fetchedDuplicatesList?.let { list ->
@ -147,9 +151,9 @@ open class SavedLoginsStorageController(
fun fetchLoginDetails(loginId: String) { fun fetchLoginDetails(loginId: String) {
var deferredLogin: Deferred<List<Login>>? = null var deferredLogin: Deferred<List<Login>>? = null
val fetchLoginJob = viewLifecycleScope.launch(Dispatchers.IO) { val fetchLoginJob = viewLifecycleScope.launch(ioDispatcher) {
deferredLogin = async { deferredLogin = async {
context.components.core.passwordsStorage.list() passwordsStorage.list()
} }
val fetchedLoginList = deferredLogin?.await() val fetchedLoginList = deferredLogin?.await()
@ -175,9 +179,9 @@ open class SavedLoginsStorageController(
fun handleLoadAndMapLogins() { fun handleLoadAndMapLogins() {
var deferredLogins: Deferred<List<Login>>? = null var deferredLogins: Deferred<List<Login>>? = null
val fetchLoginsJob = viewLifecycleScope.launch(Dispatchers.IO) { val fetchLoginsJob = viewLifecycleScope.launch(ioDispatcher) {
deferredLogins = async { deferredLogins = async {
context.components.core.passwordsStorage.list() passwordsStorage.list()
} }
val logins = deferredLogins?.await() val logins = deferredLogins?.await()
logins?.let { 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.R
import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.redirectToReAuth import org.mozilla.fenix.ext.redirectToReAuth
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -79,7 +80,7 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
interactor = EditLoginInteractor( interactor = EditLoginInteractor(
SavedLoginsStorageController( SavedLoginsStorageController(
context = requireContext(), passwordsStorage = requireContext().components.core.passwordsStorage,
viewLifecycleScope = viewLifecycleOwner.lifecycleScope, viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
navController = findNavController(), navController = findNavController(),
loginsFragmentStore = loginsFragmentStore loginsFragmentStore = loginsFragmentStore

View File

@ -94,7 +94,7 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) {
interactor = LoginDetailInteractor( interactor = LoginDetailInteractor(
SavedLoginsStorageController( SavedLoginsStorageController(
context = requireContext(), passwordsStorage = requireContext().components.core.passwordsStorage,
viewLifecycleScope = viewLifecycleOwner.lifecycleScope, viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
navController = findNavController(), navController = findNavController(),
loginsFragmentStore = savedLoginsStore loginsFragmentStore = savedLoginsStore

View File

@ -145,7 +145,9 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() {
requirePreference(R.string.pref_key_password_sync_logins), requirePreference(R.string.pref_key_password_sync_logins),
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
accountManager = requireComponents.backgroundServices.accountManager, accountManager = requireComponents.backgroundServices.accountManager,
navController = findNavController() navController = findNavController(),
accountsAuthFeature = requireComponents.services.accountsAuthFeature,
metrics = requireComponents.analytics.metrics
) )
togglePrefsEnabledWhileAuthenticating(enabled = true) togglePrefsEnabledWhileAuthenticating(enabled = true)

View File

@ -22,8 +22,8 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import kotlinx.android.synthetic.main.fragment_saved_logins.view.* import kotlinx.android.synthetic.main.fragment_saved_logins.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi import mozilla.components.concept.menu.MenuController
import mozilla.components.browser.menu.BrowserMenu import mozilla.components.concept.menu.Orientation
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -31,7 +31,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.redirectToReAuth import org.mozilla.fenix.ext.redirectToReAuth
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.settings.logins.LoginsAction import org.mozilla.fenix.settings.logins.LoginsAction
@ -51,7 +50,6 @@ class SavedLoginsFragment : Fragment() {
private lateinit var savedLoginsInteractor: SavedLoginsInteractor private lateinit var savedLoginsInteractor: SavedLoginsInteractor
private lateinit var dropDownMenuAnchorView: View private lateinit var dropDownMenuAnchorView: View
private lateinit var sortingStrategyMenu: SavedLoginsSortingStrategyMenu private lateinit var sortingStrategyMenu: SavedLoginsSortingStrategyMenu
private lateinit var sortingStrategyPopupMenu: BrowserMenu
private lateinit var toolbarChildContainer: FrameLayout private lateinit var toolbarChildContainer: FrameLayout
private lateinit var sortLoginsMenuRoot: ConstraintLayout private lateinit var sortLoginsMenuRoot: ConstraintLayout
private lateinit var loginsListController: LoginsListController private lateinit var loginsListController: LoginsListController
@ -101,7 +99,7 @@ class SavedLoginsFragment : Fragment() {
) )
savedLoginsStorageController = savedLoginsStorageController =
SavedLoginsStorageController( SavedLoginsStorageController(
context = requireContext(), passwordsStorage = requireContext().components.core.passwordsStorage,
viewLifecycleScope = viewLifecycleOwner.lifecycleScope, viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
navController = findNavController(), navController = findNavController(),
loginsFragmentStore = savedLoginsStore loginsFragmentStore = savedLoginsStore
@ -121,10 +119,8 @@ class SavedLoginsFragment : Fragment() {
return view return view
} }
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
consumeFrom(savedLoginsStore) { consumeFrom(savedLoginsStore) {
sortingStrategyMenu.updateMenu(savedLoginsStore.state.highlightedItem) sortingStrategyMenu.updateMenu(savedLoginsStore.state.highlightedItem)
savedLoginsListView.update(it) savedLoginsListView.update(it)
@ -161,7 +157,7 @@ class SavedLoginsFragment : Fragment() {
toolbarChildContainer.removeAllViews() toolbarChildContainer.removeAllViews()
toolbarChildContainer.visibility = View.GONE toolbarChildContainer.visibility = View.GONE
(activity as HomeActivity).getSupportActionBarAndInflateIfNecessary().setDisplayShowTitleEnabled(true) (activity as HomeActivity).getSupportActionBarAndInflateIfNecessary().setDisplayShowTitleEnabled(true)
sortingStrategyPopupMenu.dismiss() sortingStrategyMenu.menuController.dismiss()
redirectToReAuth(listOf(R.id.loginDetailFragment), findNavController().currentDestination?.id) redirectToReAuth(listOf(R.id.loginDetailFragment), findNavController().currentDestination?.id)
super.onPause() super.onPause()
@ -206,47 +202,27 @@ class SavedLoginsFragment : Fragment() {
} }
private fun attachMenu() { private fun attachMenu() {
sortingStrategyPopupMenu = sortingStrategyMenu.menuBuilder.build(requireContext()) sortingStrategyMenu.menuController.register(object : MenuController.Observer {
override fun onDismiss() {
sortLoginsMenuRoot.setOnClickListener { // Deactivate button on dismiss
sortLoginsMenuRoot.isActivated = true
sortingStrategyPopupMenu.show(
anchor = dropDownMenuAnchorView,
orientation = BrowserMenu.Orientation.DOWN
) {
sortLoginsMenuRoot.isActivated = false 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) { private fun setupMenu(itemToHighlight: SavedLoginsSortingStrategyMenu.Item) {
sortingStrategyMenu = sortingStrategyMenu = SavedLoginsSortingStrategyMenu(requireContext(), savedLoginsInteractor)
SavedLoginsSortingStrategyMenu( sortingStrategyMenu.updateMenu(itemToHighlight)
requireContext(),
itemToHighlight
) {
when (it) {
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort -> {
savedLoginsInteractor.onSortingStrategyChanged(
SortingStrategy.Alphabetically(
requireComponents.publicSuffixList
)
)
}
SavedLoginsSortingStrategyMenu.Item.LastUsedSort -> {
savedLoginsInteractor.onSortingStrategyChanged(
SortingStrategy.LastUsed
)
}
}
}
attachMenu() 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 package org.mozilla.fenix.settings.logins.view
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.logins_item.*
import kotlinx.android.synthetic.main.logins_item.view.*
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.settings.logins.SavedLogin import org.mozilla.fenix.settings.logins.SavedLogin
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
import org.mozilla.fenix.utils.view.ViewHolder
class LoginsListViewHolder( class LoginsListViewHolder(
private val view: View, view: View,
private val interactor: SavedLoginsInteractor 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 private var loginItem: SavedLogin? = null
fun bind(item: SavedLogin) { fun bind(item: SavedLogin) {
@ -30,17 +27,17 @@ class LoginsListViewHolder(
username = item.username, username = item.username,
timeLastUsed = item.timeLastUsed timeLastUsed = item.timeLastUsed
) )
url.text = item.origin webAddressView.text = item.origin
username.text = item.username usernameView.text = item.username
updateFavIcon(item.origin) updateFavIcon(item.origin)
view.setOnClickListener { itemView.setOnClickListener {
interactor.onItemClicked(item) interactor.onItemClicked(item)
} }
} }
private fun updateFavIcon(url: String) { 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.Preference.OnPreferenceClickListener
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.PhoneFeature
@ -78,6 +80,11 @@ class SitePermissionsFragment : PreferenceFragmentCompat() {
private fun navigateToPhoneFeature(phoneFeature: PhoneFeature) { private fun navigateToPhoneFeature(phoneFeature: PhoneFeature) {
val directions = SitePermissionsFragmentDirections val directions = SitePermissionsFragmentDirections
.actionSitePermissionsToManagePhoneFeatures(phoneFeature) .actionSitePermissionsToManagePhoneFeatures(phoneFeature)
if (phoneFeature == PhoneFeature.AUTOPLAY_AUDIBLE) {
requireComponents.analytics.metrics.track(Event.AutoPlaySettingVisited)
}
Navigation.findNavController(requireView()).navigate(directions) 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.ASK_TO_ALLOW
import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.BLOCKED import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.BLOCKED
import org.mozilla.fenix.R 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.settings
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_AUDIBLE import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_AUDIBLE
@ -180,16 +182,27 @@ class SitePermissionsManagePhoneFeatureFragment : Fragment() {
*/ */
private fun saveActionInSettings(autoplaySetting: Int) { private fun saveActionInSettings(autoplaySetting: Int) {
settings.setAutoplayUserSetting(autoplaySetting) settings.setAutoplayUserSetting(autoplaySetting)
val setting: Event.AutoPlaySettingChanged.AutoplaySetting
val (audible, inaudible) = when (autoplaySetting) { val (audible, inaudible) = when (autoplaySetting) {
AUTOPLAY_ALLOW_ALL, AUTOPLAY_ALLOW_ALL,
AUTOPLAY_ALLOW_ON_WIFI -> { AUTOPLAY_ALLOW_ON_WIFI -> {
settings.setAutoplayUserSetting(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 BLOCKED to BLOCKED
} }
AUTOPLAY_BLOCK_AUDIBLE -> BLOCKED to ALLOWED
AUTOPLAY_BLOCK_ALL -> BLOCKED to BLOCKED
else -> return else -> return
} }
requireComponents.analytics.metrics.track(Event.AutoPlaySettingChanged(setting))
settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, audible) settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, audible)
settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, inaudible) settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, inaudible)
} }

View File

@ -38,7 +38,9 @@ class PwaOnboardingDialogFragment : DialogFragment() {
add_button.setOnClickListener { add_button.setOnClickListener {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
components.useCases.webAppUseCases.addToHomescreen() 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.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.navigation.NavController
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import mozilla.components.browser.storage.sync.SyncedDeviceTabs import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.ErrorViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder
import mozilla.components.browser.storage.sync.Tab as SyncTab import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.concept.sync.Device as SyncDevice import mozilla.components.concept.sync.Device as SyncDevice
@ -24,6 +26,7 @@ class SyncedTabsAdapter(
return when (viewType) { return when (viewType) {
DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView) DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView)
TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView) TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView)
ErrorViewHolder.LAYOUT_ID -> ErrorViewHolder(itemView)
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }
@ -33,8 +36,9 @@ class SyncedTabsAdapter(
} }
override fun getItemViewType(position: Int) = when (getItem(position)) { override fun getItemViewType(position: Int) = when (getItem(position)) {
is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID
is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID
is AdapterItem.Error -> ErrorViewHolder.LAYOUT_ID
} }
fun updateData(syncedTabs: List<SyncedDeviceTabs>) { fun updateData(syncedTabs: List<SyncedDeviceTabs>) {
@ -55,7 +59,7 @@ class SyncedTabsAdapter(
when (oldItem) { when (oldItem) {
is AdapterItem.Device -> is AdapterItem.Device ->
newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id
is AdapterItem.Tab -> is AdapterItem.Tab, is AdapterItem.Error ->
oldItem == newItem oldItem == newItem
} }
@ -67,5 +71,9 @@ class SyncedTabsAdapter(
sealed class AdapterItem { sealed class AdapterItem {
data class Device(val device: SyncDevice) : AdapterItem() data class Device(val device: SyncDevice) : AdapterItem()
data class Tab(val tab: SyncTab) : 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.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout 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 androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.component_sync_tabs.view.* import kotlinx.android.synthetic.main.component_sync_tabs.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -17,6 +20,7 @@ import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.SyncedDeviceTabs import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.view.SyncedTabsView import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.R import org.mozilla.fenix.R
import java.lang.IllegalStateException
class SyncedTabsLayout @JvmOverloads constructor( class SyncedTabsLayout @JvmOverloads constructor(
context: Context, context: Context,
@ -43,10 +47,17 @@ class SyncedTabsLayout @JvmOverloads constructor(
// We may still be displaying a "loading" spinner, hide it. // We may still be displaying a "loading" spinner, hide it.
stopLoading() 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 val descriptionResId = stringResourceForError(error)
sync_tabs_status.visibility = View.VISIBLE val errorItem = getErrorItem(navController, error, descriptionResId)
val errorList: List<SyncedTabsAdapter.AdapterItem> = listOf(errorItem)
adapter.submitList(errorList)
synced_tabs_pull_to_refresh.isEnabled = pullToRefreshEnableState(error) synced_tabs_pull_to_refresh.isEnabled = pullToRefreshEnableState(error)
} }
@ -54,17 +65,11 @@ class SyncedTabsLayout @JvmOverloads constructor(
override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) { override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) {
coroutineScope.launch { coroutineScope.launch {
synced_tabs_list.visibility = View.VISIBLE
sync_tabs_status.visibility = View.GONE
adapter.updateData(syncedTabs) adapter.updateData(syncedTabs)
} }
} }
override fun startLoading() { override fun startLoading() {
synced_tabs_list.visibility = View.VISIBLE
sync_tabs_status.visibility = View.GONE
synced_tabs_pull_to_refresh.isRefreshing = true synced_tabs_pull_to_refresh.isRefreshing = true
} }
@ -78,6 +83,7 @@ class SyncedTabsLayout @JvmOverloads constructor(
} }
companion object { companion object {
internal fun pullToRefreshEnableState(error: SyncedTabsView.ErrorType) = when (error) { internal fun pullToRefreshEnableState(error: SyncedTabsView.ErrorType) = when (error) {
// Disable "pull-to-refresh" when we clearly can't sync tabs, and user needs to take an // Disable "pull-to-refresh" when we clearly can't sync tabs, and user needs to take an
// action within the app. // action within the app.
@ -94,9 +100,23 @@ class SyncedTabsLayout @JvmOverloads constructor(
internal fun stringResourceForError(error: SyncedTabsView.ErrorType) = when (error) { internal fun stringResourceForError(error: SyncedTabsView.ErrorType) = when (error) {
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device SyncedTabsView.ErrorType.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_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.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs 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 package org.mozilla.fenix.sync
import android.view.View import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView 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.sync_tabs_list_item.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_group.view.* import kotlinx.android.synthetic.main.view_synced_tabs_group.view.*
import mozilla.components.browser.storage.sync.Tab import mozilla.components.browser.storage.sync.Tab
import mozilla.components.concept.sync.DeviceType 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.R
import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem 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) { class DeviceViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) { 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) { private fun bindHeader(device: AdapterItem.Device) {
val deviceLogoDrawable = when (device.device.deviceType) { val deviceLogoDrawable = when (device.device.deviceType) {
DeviceType.DESKTOP -> R.drawable.mozac_ic_device_desktop DeviceType.DESKTOP -> R.drawable.mozac_ic_device_desktop
else -> R.drawable.mozac_ic_device_mobile else -> R.drawable.mozac_ic_device_mobile
} }
itemView.synced_tabs_group_name.text = device.device.displayName 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 { companion object {
const val LAYOUT_ID = R.layout.view_synced_tabs_group 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.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import org.mozilla.fenix.R import org.mozilla.fenix.R
data class TabHistoryItem( data class TabHistoryItem(
@ -18,23 +19,23 @@ data class TabHistoryItem(
class TabHistoryAdapter( class TabHistoryAdapter(
private val interactor: TabHistoryViewInteractor private val interactor: TabHistoryViewInteractor
) : RecyclerView.Adapter<TabHistoryViewHolder>() { ) : ListAdapter<TabHistoryItem, TabHistoryViewHolder>(DiffCallback) {
var historyList: List<TabHistoryItem> = emptyList()
set(value) {
field = value
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabHistoryViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabHistoryViewHolder {
val view = val view = LayoutInflater.from(parent.context)
LayoutInflater.from(parent.context).inflate(R.layout.history_list_item, parent, false) .inflate(R.layout.tab_history_list_item, parent, false)
return TabHistoryViewHolder(view, interactor) return TabHistoryViewHolder(view, interactor)
} }
override fun onBindViewHolder(holder: TabHistoryViewHolder, position: Int) { 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( class TabHistoryView(
private val container: ViewGroup, container: ViewGroup,
private val expandDialog: () -> Unit, private val expandDialog: () -> Unit,
interactor: TabHistoryViewInteractor interactor: TabHistoryViewInteractor
) : LayoutContainer { ) : LayoutContainer {
override val containerView: View? override val containerView: View = LayoutInflater.from(container.context)
get() = container
val view: View = LayoutInflater.from(container.context)
.inflate(R.layout.component_tabhistory, container, true) .inflate(R.layout.component_tabhistory, container, true)
private val adapter = TabHistoryAdapter(interactor) 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?) { override fun onLayoutCompleted(state: RecyclerView.State?) {
super.onLayoutCompleted(state) super.onLayoutCompleted(state)
currentIndex?.let { index -> currentIndex?.let { index ->
@ -73,7 +70,7 @@ class TabHistoryView(
isSelected = index == historyState.currentIndex isSelected = index == historyState.currentIndex
) )
} }
adapter.historyList = items adapter.submitList(items)
} }
} }
} }

View File

@ -5,30 +5,37 @@
package org.mozilla.fenix.tabhistory package org.mozilla.fenix.tabhistory
import android.view.View import android.view.View
import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.tab_history_list_item.*
import kotlinx.android.synthetic.main.history_list_item.view.* 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( class TabHistoryViewHolder(
private val view: View, view: View,
private val interactor: TabHistoryViewInteractor 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) { fun bind(item: TabHistoryItem) {
view.history_layout.overflowView.isVisible = false this.item = item
view.history_layout.urlView.text = item.url
view.history_layout.loadFavicon(item.url)
view.history_layout.titleView.text = if (item.isSelected) { history_layout.displayAs(LibrarySiteItemView.ItemType.SITE)
buildSpannedString { history_layout.overflowView.isVisible = false
bold { append(item.title) } 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 { } 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -19,7 +20,8 @@ import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.ViewHolder
* multiple [RecyclerView.Adapter] in one [RecyclerView]. * multiple [RecyclerView.Adapter] in one [RecyclerView].
*/ */
class SaveToCollectionsButtonAdapter( class SaveToCollectionsButtonAdapter(
private val interactor: TabTrayInteractor private val interactor: TabTrayInteractor,
private val isPrivate: Boolean = false
) : ListAdapter<Item, ViewHolder>(DiffCallback) { ) : ListAdapter<Item, ViewHolder>(DiffCallback) {
init { init {
@ -31,7 +33,26 @@ class SaveToCollectionsButtonAdapter(
return ViewHolder(itemView, interactor) 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 { override fun getItemViewType(position: Int): Int {
return ViewHolder.LAYOUT_ID return ViewHolder.LAYOUT_ID
@ -43,6 +64,10 @@ class SaveToCollectionsButtonAdapter(
override fun areContentsTheSame(oldItem: Item, newItem: Item) = true override fun areContentsTheSame(oldItem: Item, newItem: Item) = true
} }
enum class MultiselectModeChange {
MULTISELECT, NORMAL
}
/** /**
* An object to identify the data type. * An object to identify the data type.
*/ */

View File

@ -8,13 +8,15 @@ import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session 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.engine.prompt.ShareData
import mozilla.components.concept.tabstray.Tab import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.home.HomeFragment
@ -41,7 +43,9 @@ interface TabTrayController {
/** /**
* Default behavior of [TabTrayController]. Other implementations are possible. * 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 navController [NavController] used for navigation.
* @param dismissTabTray callback allowing to request this entire Fragment to be dismissed. * @param dismissTabTray callback allowing to request this entire Fragment to be dismissed.
* @param tabTrayDialogFragmentStore [TabTrayDialogFragmentStore] holding the State for all Views displayed * @param tabTrayDialogFragmentStore [TabTrayDialogFragmentStore] holding the State for all Views displayed
@ -55,7 +59,10 @@ interface TabTrayController {
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class DefaultTabTrayController( 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 navController: NavController,
private val dismissTabTray: () -> Unit, private val dismissTabTray: () -> Unit,
private val dismissTabTrayAndNavigateHome: (String) -> Unit, private val dismissTabTrayAndNavigateHome: (String) -> Unit,
@ -65,14 +72,13 @@ class DefaultTabTrayController(
private val showChooseCollectionDialog: (List<Session>) -> Unit, private val showChooseCollectionDialog: (List<Session>) -> Unit,
private val showAddNewCollectionDialog: (List<Session>) -> Unit private val showAddNewCollectionDialog: (List<Session>) -> Unit
) : TabTrayController { ) : TabTrayController {
private val tabCollectionStorage = activity.components.core.tabCollectionStorage
override fun onNewTabTapped(private: Boolean) { override fun onNewTabTapped(private: Boolean) {
val startTime = activity.components.core.engine.profiler?.getProfilerTime() val startTime = profiler?.getProfilerTime()
activity.browsingModeManager.mode = BrowsingMode.fromBoolean(private) browsingModeManager.mode = BrowsingMode.fromBoolean(private)
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
dismissTabTray() dismissTabTray()
activity.components.core.engine.profiler?.addMarker( profiler?.addMarker(
"DefaultTabTrayController.onNewTabTapped", "DefaultTabTrayController.onNewTabTapped",
startTime startTime
) )
@ -84,7 +90,7 @@ class DefaultTabTrayController(
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) { override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
val sessionList = selectedTabs.map { 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 // Only register the observer right before moving to collection creation
@ -141,7 +147,7 @@ class DefaultTabTrayController(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
private fun getListOfSessions(private: Boolean): List<Session> { 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 { override fun onModeRequested(): TabTrayDialogFragmentState.Mode {

View File

@ -163,7 +163,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) 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 thumbnailLoader = ThumbnailLoader(requireContext().components.core.thumbnailStorage)
val adapter = FenixTabsAdapter(requireContext(), thumbnailLoader) val adapter = FenixTabsAdapter(requireContext(), thumbnailLoader)
@ -173,7 +174,10 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
adapter, adapter,
interactor = TabTrayFragmentInteractor( interactor = TabTrayFragmentInteractor(
DefaultTabTrayController( 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(), navController = findNavController(),
dismissTabTray = ::dismissAllowingStateLoss, dismissTabTray = ::dismissAllowingStateLoss,
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome, dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,

View File

@ -41,6 +41,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.MultiselectModeChange
/** /**
* View that contains and configures the BrowserAwesomeBar * View that contains and configures the BrowserAwesomeBar
@ -70,8 +71,9 @@ class TabTrayView(
private val tabTrayItemMenu: TabTrayItemMenu private val tabTrayItemMenu: TabTrayItemMenu
private var menu: BrowserMenu? = null private var menu: BrowserMenu? = null
private val bottomSheetCallback: BottomSheetBehavior.BottomSheetCallback
private var tabsTouchHelper: TabsTouchHelper private var tabsTouchHelper: TabsTouchHelper
private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor) private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate)
private var hasLoaded = false private var hasLoaded = false
@ -83,7 +85,7 @@ class TabTrayView(
toggleFabText(isPrivate) toggleFabText(isPrivate)
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) { override fun onSlide(bottomSheet: View, slideOffset: Float) {
if (!hasAccessibilityEnabled) { if (!hasAccessibilityEnabled) {
if (slideOffset >= SLIDE_OFFSET) { if (slideOffset >= SLIDE_OFFSET) {
@ -100,7 +102,9 @@ class TabTrayView(
interactor.onTabTrayDismissed() interactor.onTabTrayDismissed()
} }
} }
}) }
behavior.addBottomSheetCallback(bottomSheetCallback)
val selectedTabIndex = if (!isPrivate) { val selectedTabIndex = if (!isPrivate) {
DEFAULT_TAB_ID DEFAULT_TAB_ID
@ -230,9 +234,21 @@ class TabTrayView(
behavior.state = BottomSheetBehavior.STATE_EXPANDED 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?) { override fun onTabSelected(tab: TabLayout.Tab?) {
toggleFabText(isPrivateModeSelected) toggleFabText(isPrivateModeSelected)
filterTabs.invoke(isPrivateModeSelected) filterTabs.invoke(isPrivateModeSelected)
toggleSaveToCollectionButton(isPrivateModeSelected)
updateUINormalMode(view.context.components.core.store.state) updateUINormalMode(view.context.components.core.store.state)
scrollToTab(view.context.components.core.store.state.selectedTabId) scrollToTab(view.context.components.core.store.state.selectedTabId)
@ -257,7 +273,7 @@ class TabTrayView(
val oldMode = mode val oldMode = mode
if (oldMode::class != state.mode::class) { if (oldMode::class != state.mode::class) {
updateTabsForModeChanged() updateTabsForMultiselectModeChanged(state.mode is TabTrayDialogFragmentState.Mode.MultiSelect)
if (view.context.settings().accessibilityServicesEnabled) { if (view.context.settings().accessibilityServicesEnabled) {
view.announceForAccessibility( view.announceForAccessibility(
if (state.mode == TabTrayDialogFragmentState.Mode.Normal) view.context.getString( if (state.mode == TabTrayDialogFragmentState.Mode.Normal) view.context.getString(
@ -273,6 +289,7 @@ class TabTrayView(
view.tabsTray.apply { view.tabsTray.apply {
tabsTouchHelper.attachToRecyclerView(this) tabsTouchHelper.attachToRecyclerView(this)
} }
behavior.addBottomSheetCallback(bottomSheetCallback)
toggleUIMultiselect(multiselect = false) toggleUIMultiselect(multiselect = false)
@ -281,6 +298,7 @@ class TabTrayView(
is TabTrayDialogFragmentState.Mode.MultiSelect -> { is TabTrayDialogFragmentState.Mode.MultiSelect -> {
// Disable swipe to delete while in multiselect // Disable swipe to delete while in multiselect
tabsTouchHelper.attachToRecyclerView(null) tabsTouchHelper.attachToRecyclerView(null)
behavior.removeBottomSheetCallback(bottomSheetCallback)
toggleUIMultiselect(multiselect = true) toggleUIMultiselect(multiselect = true)
@ -402,13 +420,18 @@ class TabTrayView(
view.tab_layout.isVisible = !multiselect view.tab_layout.isVisible = !multiselect
} }
private fun updateTabsForModeChanged() { private fun updateTabsForMultiselectModeChanged(inMultiselectMode: Boolean) {
view.tabsTray.apply { view.tabsTray.apply {
val tabs = view.context.components.core.store.state.getNormalOrPrivateTabs( val tabs = view.context.components.core.store.state.getNormalOrPrivateTabs(
isPrivateModeSelected 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 } val selectedBrowserTabIndex = tabs.indexOfFirst { it.id == itemId }
this.adapter?.notifyItemChanged( tabsAdapter.notifyItemChanged(
selectedBrowserTabIndex, true 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.base.observer.Observable
import mozilla.components.support.images.ImageLoadRequest import mozilla.components.support.images.ImageLoadRequest
import mozilla.components.support.images.loader.ImageLoader import mozilla.components.support.images.loader.ImageLoader
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController 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.removeAndDisable
import org.mozilla.fenix.ext.removeTouchDelegate import org.mozilla.fenix.ext.removeTouchDelegate
import org.mozilla.fenix.ext.showAndEnable import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.utils.Do import org.mozilla.fenix.utils.Do
import kotlin.math.max import kotlin.math.max
@ -160,7 +160,9 @@ class TabTrayViewHolder(
// is done in the toolbar and awesomebar: // is done in the toolbar and awesomebar:
// https://github.com/mozilla-mobile/fenix/issues/1824 // https://github.com/mozilla-mobile/fenix/issues/1824
// https://github.com/mozilla-mobile/android-components/issues/6985 // 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 @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.deletebrowsingdata.DeleteBrowsingDataOnQuitType
import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu
import org.mozilla.fenix.settings.logins.SortingStrategy import org.mozilla.fenix.settings.logins.SortingStrategy
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsFragment
import org.mozilla.fenix.settings.registerOnSharedPreferenceChangeListener import org.mozilla.fenix.settings.registerOnSharedPreferenceChangeListener
import java.security.InvalidParameterException import java.security.InvalidParameterException
@ -324,6 +323,15 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = true 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 { fun isDefaultBrowser(): Boolean {
val browsers = BrowsersCache.all(appContext) val browsers = BrowsersCache.all(appContext)
return browsers.isDefaultBrowser return browsers.isDefaultBrowser
@ -811,36 +819,26 @@ class Settings(private val appContext: Context) : PreferencesHolder {
private var savedLoginsSortingStrategyString by stringPreference( private var savedLoginsSortingStrategyString by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_saved_logins_sorting_strategy), appContext.getPreferenceKey(R.string.pref_key_saved_logins_sorting_strategy),
default = SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY default = SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort.strategyString
) )
val savedLoginsMenuHighlightedItem: SavedLoginsSortingStrategyMenu.Item val savedLoginsMenuHighlightedItem: SavedLoginsSortingStrategyMenu.Item
get() { get() = SavedLoginsSortingStrategyMenu.Item.fromString(savedLoginsSortingStrategyString)
return when (savedLoginsSortingStrategyString) {
SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY -> {
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort
}
SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> {
SavedLoginsSortingStrategyMenu.Item.LastUsedSort
}
else -> SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort
}
}
var savedLoginsSortingStrategy: SortingStrategy var savedLoginsSortingStrategy: SortingStrategy
get() { get() {
return when (savedLoginsSortingStrategyString) { return when (savedLoginsMenuHighlightedItem) {
SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY -> SortingStrategy.Alphabetically( SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort ->
appContext.components.publicSuffixList SortingStrategy.Alphabetically(appContext.components.publicSuffixList)
) SavedLoginsSortingStrategyMenu.Item.LastUsedSort -> SortingStrategy.LastUsed
SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> SortingStrategy.LastUsed
else -> SortingStrategy.Alphabetically(appContext.components.publicSuffixList)
} }
} }
set(value) { set(value) {
savedLoginsSortingStrategyString = when (value) { savedLoginsSortingStrategyString = when (value) {
is SortingStrategy.Alphabetically -> SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY is SortingStrategy.Alphabetically ->
is SortingStrategy.LastUsed -> SavedLoginsFragment.SORTING_STRATEGY_LAST_USED SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort.strategyString
is SortingStrategy.LastUsed ->
SavedLoginsSortingStrategyMenu.Item.LastUsedSort.strategyString
} }
} }
} }

View File

@ -32,8 +32,11 @@ object ToolbarPopupWindow {
copyVisible: Boolean = true copyVisible: Boolean = true
) { ) {
val context = view.get()?.context ?: return val context = view.get()?.context ?: return
val isCustomTabSession = customTabSession != null
val clipboard = context.components.clipboardHandler val clipboard = context.components.clipboardHandler
if (!copyVisible && clipboard.text.isNullOrEmpty()) return
val isCustomTabSession = customTabSession != null
val customView = LayoutInflater.from(context) val customView = LayoutInflater.from(context)
.inflate(R.layout.browser_toolbar_popup_window, null) .inflate(R.layout.browser_toolbar_popup_window, null)
val popupWindow = PopupWindow( val popupWindow = PopupWindow(

View File

@ -23,6 +23,8 @@ import androidx.core.graphics.drawable.toBitmap
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R 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.ext.settings
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
import org.mozilla.fenix.widget.VoiceSearchActivity import org.mozilla.fenix.widget.VoiceSearchActivity
@ -36,6 +38,7 @@ class SearchWidgetProvider : AppWidgetProvider() {
override fun onEnabled(context: Context) { override fun onEnabled(context: Context) {
context.settings().addSearchWidgetInstalled(1) context.settings().addSearchWidgetInstalled(1)
context.metrics.track(Event.SearchWidgetInstalled)
} }
override fun onDeleted(context: Context, appWidgetIds: IntArray) { override fun onDeleted(context: Context, appWidgetIds: IntArray) {

View File

@ -9,5 +9,5 @@
android:width="14dp" android:width="14dp"
android:height="14dp" /> android:height="14dp" />
<solid android:color="?accentBright" /> <solid android:color="?accentBright" />
<stroke android:color="@color/light_grey_05"/> <stroke android:color="@color/photonLightGrey05"/>
</shape> </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_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<include layout="@layout/fragment_browser" />
<org.mozilla.fenix.browser.TabPreview <org.mozilla.fenix.browser.TabPreview
android:id="@+id/tabPreview" android:id="@+id/tabPreview"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -45,5 +45,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" 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> </FrameLayout>

View File

@ -20,20 +20,6 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="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 <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/synced_tabs_pull_to_refresh" android:id="@+id/synced_tabs_pull_to_refresh"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -2,7 +2,8 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public <!-- 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 - License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - file, 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" android:id="@+id/removeAllExceptions"
style="@style/DestructiveButton" style="@style/DestructiveButton"
android:layout_marginHorizontal="16dp" 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 - License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android" <TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/exceptions_description" android:id="@+id/exceptions_description"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="12dp" android:layout_margin="12dp"
android:text="@string/enhanced_tracking_protection_exceptions" tools:text="@string/enhanced_tracking_protection_exceptions"
android:textColor="?primaryText" android:textColor="?primaryText"
android:textSize="16sp" /> android:textSize="16sp" />

View File

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