Merge branch 'master' of https://github.com/mozilla-mobile/fenix
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
commit
4e2a9d6999
|
@ -1,9 +1,9 @@
|
|||
# Firefox Preview
|
||||
# Firefox for Android
|
||||
|
||||
[![Task Status](https://github.taskcluster.net/v1/repository/mozilla-mobile/fenix/master/badge.svg)](https://github.taskcluster.net/v1/repository/mozilla-mobile/fenix/master/latest)
|
||||
[![codecov](https://codecov.io/gh/mozilla-mobile/fenix/branch/master/graph/badge.svg)](https://codecov.io/gh/mozilla-mobile/fenix)
|
||||
|
||||
Firefox Preview (internal code name: "Fenix") is an all-new browser for Android, based on [GeckoView](https://mozilla.github.io/geckoview/) and [Mozilla Android Components](https://mozac.org/).
|
||||
Fenix (internal codename) is the all-new Firefox for Android browser, based on [GeckoView](https://mozilla.github.io/geckoview/) and [Mozilla Android Components](https://mozac.org/).
|
||||
|
||||
** Note: The team is currently experiencing heavy triage and review load, so when triaging issues, we will mainly be looking to identify [S1 (high severity)](https://github.com/mozilla-mobile/fenix/labels/S1) issues. See our triage process [here](https://github.com/mozilla-mobile/fenix/wiki/Triage-Process). Please be patient if you don't hear back from us immediately on your issue! **
|
||||
|
||||
|
|
|
@ -410,6 +410,7 @@ dependencies {
|
|||
implementation Deps.mozilla_browser_domains
|
||||
implementation Deps.mozilla_browser_icons
|
||||
implementation Deps.mozilla_browser_menu
|
||||
implementation Deps.mozilla_browser_menu2
|
||||
implementation Deps.mozilla_browser_search
|
||||
implementation Deps.mozilla_browser_session
|
||||
implementation Deps.mozilla_browser_state
|
||||
|
@ -469,7 +470,8 @@ dependencies {
|
|||
|
||||
implementation Deps.mozilla_ui_colors
|
||||
implementation Deps.mozilla_ui_icons
|
||||
implementation Deps.mozilla_ui_publicsuffixlist
|
||||
implementation Deps.mozilla_lib_publicsuffixlist
|
||||
implementation Deps.mozilla_ui_widgets
|
||||
|
||||
implementation Deps.mozilla_lib_crash
|
||||
implementation Deps.mozilla_lib_push_firebase
|
||||
|
|
212
app/metrics.yaml
212
app/metrics.yaml
|
@ -471,6 +471,52 @@ context_menu:
|
|||
- fenix-core@mozilla.com
|
||||
expires: "2020-10-01"
|
||||
|
||||
login_dialog:
|
||||
displayed:
|
||||
type: event
|
||||
description: |
|
||||
The login dialog prompt was displayed
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/9730
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/13050
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
expires: "2021-02-01"
|
||||
cancelled:
|
||||
type: event
|
||||
description: |
|
||||
The login dialog prompt was cancelled
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/9730
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/13050
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
expires: "2021-02-01"
|
||||
saved:
|
||||
type: event
|
||||
description: |
|
||||
The login dialog prompt "save" button was pressed
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/9730
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/13050
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
expires: "2021-02-01"
|
||||
never_save:
|
||||
type: event
|
||||
description: |
|
||||
The login dialog prompt "never save" button was pressed
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/9730
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/13050
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
expires: "2021-02-01"
|
||||
|
||||
find_in_page:
|
||||
opened:
|
||||
type: event
|
||||
|
@ -3161,3 +3207,169 @@ perf.awesomebar:
|
|||
- fenix-core@mozilla.com
|
||||
- gkruglov@mozilla.com
|
||||
expires: "2020-10-01"
|
||||
|
||||
autoplay:
|
||||
visited_setting:
|
||||
type: event
|
||||
description: A user visited the autoplay settings screen
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/11579
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/13041#issuecomment-665777411
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
expires: "2021-02-01"
|
||||
setting_changed:
|
||||
type: event
|
||||
description: |
|
||||
A user changed their autoplay setting to either block_cellular,
|
||||
block_audio, or block_all.
|
||||
extra_keys:
|
||||
autoplay_setting:
|
||||
description: |
|
||||
The new setting for autoplay: block_cellular,
|
||||
block_audio, or block_all.
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/11579
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/13041#issuecomment-665777411
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
expires: "2021-02-01"
|
||||
|
||||
storage.stats:
|
||||
query_stats_duration:
|
||||
send_in_pings:
|
||||
- metrics
|
||||
type: timing_distribution
|
||||
description: >
|
||||
How long it took to query the device for the StorageStats that contain the
|
||||
file size information. The docs say it may be expensive so we want to
|
||||
ensure it's not too expensive. This value is only available on Android
|
||||
8+.
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/12802
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
- perf-android-fe@mozilla.com
|
||||
- mcomella@mozilla.com
|
||||
expires: "2021-02-01"
|
||||
app_bytes:
|
||||
send_in_pings:
|
||||
- metrics
|
||||
type: memory_distribution
|
||||
description: >
|
||||
The size of the app's APK and related files as installed: this is expected
|
||||
to be larger than download size. This is the output of
|
||||
[StorageStats.getAppBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getAppBytes())
|
||||
so see that for details. This value is only available on Android 8+. A
|
||||
similar value may be available on the Google Play dashboard: we can use
|
||||
this value to see if that value is reliable enough.
|
||||
memory_unit: byte
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/12802
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
- perf-android-fe@mozilla.com
|
||||
- mcomella@mozilla.com
|
||||
expires: "2021-02-01"
|
||||
cache_bytes:
|
||||
send_in_pings:
|
||||
- metrics
|
||||
type: memory_distribution
|
||||
description: >
|
||||
The size of all cached data in the app. This is the output of
|
||||
[StorageStats.getCacheBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getCacheBytes())
|
||||
so see that for details. This value is only available on Android 8+.
|
||||
memory_unit: byte
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/12802
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
- perf-android-fe@mozilla.com
|
||||
- mcomella@mozilla.com
|
||||
expires: "2021-02-01"
|
||||
data_dir_bytes:
|
||||
send_in_pings:
|
||||
- metrics
|
||||
type: memory_distribution
|
||||
description: >
|
||||
The size of all data minus `cache_bytes`. This is the output of
|
||||
[StorageStats.getDataBytes](https://developer.android.com/reference/android/app/usage/StorageStats#getDataBytes())
|
||||
except we subtract the value of `cache_bytes` so the cache is not measured
|
||||
redundantly; see that method for details. This value is only available on
|
||||
Android 8+.
|
||||
memory_unit: byte
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/12802
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
- perf-android-fe@mozilla.com
|
||||
- mcomella@mozilla.com
|
||||
expires: "2021-02-01"
|
||||
|
||||
progressive_web_app:
|
||||
homescreen_tap:
|
||||
type: event
|
||||
description: |
|
||||
A user taps on PWA homescreen icon
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/10261
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/11859
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
- erichards@mozilla.com
|
||||
expires: "2021-03-01"
|
||||
install_tap:
|
||||
type: event
|
||||
description: |
|
||||
A user installs a PWA. Could be a shortcut or added to homescreen.
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/10261
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/11859
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
- erichards@mozilla.com
|
||||
expires: "2021-03-01"
|
||||
foreground:
|
||||
type: event
|
||||
description: |
|
||||
A user brings the PWA into the foreground.
|
||||
extra_keys:
|
||||
time_ms:
|
||||
description: |
|
||||
The current time in ms when the PWA was brought to the foreground.
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/10261
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/11859
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
- erichards@mozilla.com
|
||||
expires: "2021-03-01"
|
||||
background:
|
||||
type: event
|
||||
description: |
|
||||
A user puts the PWA into the background.
|
||||
extra_keys:
|
||||
time_ms:
|
||||
description: |
|
||||
The current time in ms when the PWA was backgrounded.
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/10261
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/pull/11859
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
- erichards@mozilla.com
|
||||
expires: "2021-03-01"
|
||||
|
|
|
@ -10,6 +10,7 @@ import okhttp3.mockwebserver.MockWebServer
|
|||
import org.junit.Rule
|
||||
import org.junit.Before
|
||||
import org.junit.After
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
|
||||
import org.mozilla.fenix.helpers.HomeActivityTestRule
|
||||
|
@ -74,6 +75,7 @@ class SettingsAddonsTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13220")
|
||||
// Opens the addons settings menu, installs an addon, then uninstalls
|
||||
@Test
|
||||
fun verifyAddonsCanBeUninstalled() {
|
||||
|
|
|
@ -63,6 +63,7 @@ class SmokeTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13217")
|
||||
@Test
|
||||
fun verifyPageMainMenuItemsListInPortraitNormalModeTest() {
|
||||
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
|
||||
|
@ -140,6 +141,7 @@ class SmokeTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13217")
|
||||
@Test
|
||||
fun verifyPageMainMenuItemsListInPortraitPrivateModeTest() {
|
||||
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
|
||||
|
|
|
@ -6,9 +6,7 @@ package org.mozilla.fenix
|
|||
|
||||
enum class ReleaseChannel {
|
||||
FenixDebug,
|
||||
|
||||
FenixProduction,
|
||||
|
||||
FennecProduction,
|
||||
FennecBeta;
|
||||
|
||||
|
@ -35,6 +33,12 @@ enum class ReleaseChannel {
|
|||
else -> false
|
||||
}
|
||||
|
||||
val isRelease: Boolean
|
||||
get() = when (this) {
|
||||
FennecProduction -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val isBeta: Boolean
|
||||
get() = when (this) {
|
||||
FennecBeta -> true
|
||||
|
|
|
@ -44,6 +44,7 @@ import org.mozilla.fenix.components.Components
|
|||
import org.mozilla.fenix.components.metrics.MetricServiceType
|
||||
import org.mozilla.fenix.ext.resetPoliciesAfter
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.perf.StorageStatsMetrics
|
||||
import org.mozilla.fenix.perf.StartupTimeline
|
||||
import org.mozilla.fenix.push.PushFxaIntegration
|
||||
import org.mozilla.fenix.push.WebPushEngineIntegration
|
||||
|
@ -205,12 +206,24 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
|
|||
}
|
||||
}
|
||||
|
||||
fun queueMetrics() {
|
||||
if (SDK_INT >= Build.VERSION_CODES.O) { // required by StorageStatsMetrics.
|
||||
taskQueue.runIfReadyOrQueue {
|
||||
// Because it may be slow to capture the storage stats, it might be preferred to
|
||||
// create a WorkManager task for this metric, however, I ran out of
|
||||
// implementation time and WorkManager is harder to test.
|
||||
StorageStatsMetrics.report(this.applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initQueue()
|
||||
|
||||
// We init these items in the visual completeness queue to avoid them initing in the critical
|
||||
// startup path, before the UI finishes drawing (i.e. visual completeness).
|
||||
queueInitExperiments()
|
||||
queueInitStorageAndServices()
|
||||
queueMetrics()
|
||||
}
|
||||
|
||||
private fun startMetricsIfEnabled() {
|
||||
|
|
|
@ -31,6 +31,7 @@ import androidx.navigation.ui.NavigationUI
|
|||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -66,8 +67,10 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
|||
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
|
||||
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
|
||||
import org.mozilla.fenix.ext.alreadyOnDestination
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.metrics
|
||||
import org.mozilla.fenix.ext.nav
|
||||
import org.mozilla.fenix.ext.resetPoliciesAfter
|
||||
import org.mozilla.fenix.ext.settings
|
||||
|
@ -96,7 +99,6 @@ import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
|
|||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
||||
import org.mozilla.fenix.theme.DefaultThemeManager
|
||||
import org.mozilla.fenix.theme.ThemeManager
|
||||
import org.mozilla.fenix.trackingprotectionexceptions.TrackingProtectionExceptionsFragmentDirections
|
||||
import org.mozilla.fenix.utils.BrowsersCache
|
||||
|
||||
/**
|
||||
|
@ -243,12 +245,28 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Launch this on a background thread so as not to affect startup performance
|
||||
lifecycleScope.launch(IO) {
|
||||
if (
|
||||
settings().isDefaultBrowser() &&
|
||||
settings().wasDefaultBrowserOnLastPause != settings().isDefaultBrowser()
|
||||
) {
|
||||
metrics.track(Event.ChangedToDefaultBrowser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final override fun onPause() {
|
||||
if (settings().lastKnownMode.isPrivate) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
|
||||
if (settings().wasDefaultBrowserOnLastPause != settings().isDefaultBrowser()
|
||||
) {
|
||||
settings().wasDefaultBrowserOnLastPause = settings().isDefaultBrowser()
|
||||
}
|
||||
|
||||
super.onPause()
|
||||
|
||||
// Every time the application goes into the background, it is possible that the user
|
||||
|
|
|
@ -17,10 +17,10 @@ import kotlinx.android.extensions.LayoutContainer
|
|||
import kotlinx.android.synthetic.main.fragment_add_on_details.*
|
||||
import mozilla.components.feature.addons.Addon
|
||||
import mozilla.components.feature.addons.ui.translatedDescription
|
||||
import mozilla.components.feature.addons.ui.updatedAtDate
|
||||
import org.mozilla.fenix.R
|
||||
import java.text.DateFormat
|
||||
import java.text.NumberFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
interface AddonDetailsInteractor {
|
||||
|
@ -44,7 +44,6 @@ class AddonDetailsView(
|
|||
private val interactor: AddonDetailsInteractor
|
||||
) : LayoutContainer {
|
||||
|
||||
private val dateParser = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
|
||||
private val dateFormatter = DateFormat.getDateInstance()
|
||||
private val numberFormatter = NumberFormat.getNumberInstance(Locale.getDefault())
|
||||
|
||||
|
@ -76,7 +75,7 @@ class AddonDetailsView(
|
|||
}
|
||||
|
||||
private fun bindLastUpdated(addon: Addon) {
|
||||
last_updated_text.text = formatDate(addon.updatedAt)
|
||||
last_updated_text.text = dateFormatter.format(addon.updatedAtDate)
|
||||
}
|
||||
|
||||
private fun bindVersion(addon: Addon) {
|
||||
|
@ -132,8 +131,4 @@ class AddonDetailsView(
|
|||
spannableStringBuilder.setSpan(clickable, start, end, flags)
|
||||
spannableStringBuilder.removeSpan(link)
|
||||
}
|
||||
|
||||
private fun formatDate(text: String): String {
|
||||
return dateFormatter.format(dateParser.parse(text)!!)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ import mozilla.components.feature.accounts.FxaWebChannelFeature
|
|||
import mozilla.components.feature.app.links.AppLinksFeature
|
||||
import mozilla.components.feature.contextmenu.ContextMenuCandidate
|
||||
import mozilla.components.feature.contextmenu.ContextMenuFeature
|
||||
import mozilla.components.feature.downloads.AbstractFetchDownloadService
|
||||
import mozilla.components.feature.downloads.DownloadsFeature
|
||||
import mozilla.components.feature.downloads.manager.FetchDownloadManager
|
||||
import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID
|
||||
|
@ -104,7 +103,6 @@ import org.mozilla.fenix.ext.hideToolbar
|
|||
import org.mozilla.fenix.ext.metrics
|
||||
import org.mozilla.fenix.ext.nav
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.sessionsOfType
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.home.SharedViewModel
|
||||
import org.mozilla.fenix.theme.ThemeManager
|
||||
|
@ -169,9 +167,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
|||
customTabSessionId = arguments?.getString(EXTRA_SESSION_ID)
|
||||
|
||||
val view = if (FeatureFlags.browserChromeGestures) {
|
||||
inflater.inflate(R.layout.browser_gesture_wrapper, container, false).apply {
|
||||
inflater.inflate(R.layout.fragment_browser, this as SwipeGestureLayout, true)
|
||||
}
|
||||
inflater.inflate(R.layout.browser_gesture_wrapper, container, false)
|
||||
} else {
|
||||
inflater.inflate(R.layout.fragment_browser, container, false)
|
||||
}
|
||||
|
@ -379,8 +375,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
|||
|
||||
downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus ->
|
||||
// If the download is just paused, don't show any in-app notification
|
||||
if (downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.COMPLETED ||
|
||||
downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.FAILED
|
||||
if (downloadJobStatus == DownloadState.Status.COMPLETED ||
|
||||
downloadJobStatus == DownloadState.Status.FAILED
|
||||
) {
|
||||
|
||||
saveDownloadDialogState(
|
||||
|
@ -392,7 +388,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
|||
val dynamicDownloadDialog = DynamicDownloadDialog(
|
||||
container = view.browserLayout,
|
||||
downloadState = downloadState,
|
||||
didFail = downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.FAILED,
|
||||
didFail = downloadJobStatus == DownloadState.Status.FAILED,
|
||||
tryAgain = downloadFeature::tryAgain,
|
||||
onCannotOpenFile = {
|
||||
FenixSnackbar.make(
|
||||
|
@ -617,12 +613,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
|||
private fun saveDownloadDialogState(
|
||||
sessionId: String?,
|
||||
downloadState: DownloadState,
|
||||
downloadJobStatus: AbstractFetchDownloadService.DownloadJobStatus
|
||||
downloadJobStatus: DownloadState.Status
|
||||
) {
|
||||
sessionId?.let { id ->
|
||||
sharedViewModel.downloadDialogState[id] = Pair(
|
||||
downloadState,
|
||||
downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.FAILED
|
||||
downloadJobStatus == DownloadState.Status.FAILED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -743,7 +739,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
|||
}
|
||||
|
||||
@CallSuper
|
||||
final override fun onPause() {
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (findNavController().currentDestination?.id != R.id.searchFragment) {
|
||||
view?.hideKeyboard()
|
||||
|
@ -835,14 +831,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
|||
sessionManager.remove(session)
|
||||
true
|
||||
} else {
|
||||
val isLastSession =
|
||||
sessionManager.sessionsOfType(private = session.private).count() == 1
|
||||
if (session.hasParentSession) {
|
||||
sessionManager.remove(session, true)
|
||||
}
|
||||
// We want to return to home if this removed session was the last session of its type
|
||||
// and didn't have a parent session to select.
|
||||
val goToOverview = isLastSession && !session.hasParentSession
|
||||
// We want to return to home if this session didn't have a parent session to select.
|
||||
val goToOverview = !session.hasParentSession
|
||||
!goToOverview
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import android.animation.AnimatorListenerAdapter
|
|||
import android.app.Activity
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
|
@ -17,7 +16,6 @@ import androidx.annotation.Dimension
|
|||
import androidx.annotation.Dimension.DP
|
||||
import androidx.core.graphics.contains
|
||||
import androidx.core.graphics.toPoint
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation
|
||||
import androidx.dynamicanimation.animation.FlingAnimation
|
||||
|
@ -25,6 +23,8 @@ import mozilla.components.browser.session.Session
|
|||
import mozilla.components.browser.session.SessionManager
|
||||
import mozilla.components.support.ktx.android.util.dpToPx
|
||||
import mozilla.components.support.ktx.android.view.getRectWithViewLocation
|
||||
import org.mozilla.fenix.ext.getWindowInsets
|
||||
import org.mozilla.fenix.ext.isKeyboardVisible
|
||||
import org.mozilla.fenix.ext.sessionsOfType
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import kotlin.math.abs
|
||||
|
@ -35,7 +35,6 @@ import kotlin.math.min
|
|||
* Handles intercepting touch events on the toolbar for swipe gestures and executes the
|
||||
* necessary animations.
|
||||
*/
|
||||
@Suppress("LargeClass", "TooManyFunctions")
|
||||
class ToolbarGestureHandler(
|
||||
private val activity: Activity,
|
||||
private val contentLayout: View,
|
||||
|
@ -56,18 +55,6 @@ class ToolbarGestureHandler(
|
|||
private val windowWidth: Int
|
||||
get() = activity.resources.displayMetrics.widthPixels
|
||||
|
||||
private val windowInsets: WindowInsetsCompat?
|
||||
get() =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// In theory, the rootWindowInsets should exist at this point but if the decorView is
|
||||
// not attached for some reason we'll get a NullPointerException without the check.
|
||||
activity.window.decorView.rootWindowInsets?.let {
|
||||
WindowInsetsCompat.toWindowInsetsCompat(it)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
private val previewOffset = PREVIEW_OFFSET.dpToPx(activity.resources.displayMetrics)
|
||||
|
||||
private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop
|
||||
|
@ -89,7 +76,12 @@ class ToolbarGestureHandler(
|
|||
GestureDirection.LEFT_TO_RIGHT
|
||||
}
|
||||
|
||||
return if (start.isInToolbar() && abs(dx) > touchSlop && abs(dy) < abs(dx)) {
|
||||
return if (
|
||||
!activity.window.decorView.isKeyboardVisible() &&
|
||||
start.isInToolbar() &&
|
||||
abs(dx) > touchSlop &&
|
||||
abs(dy) < abs(dx)
|
||||
) {
|
||||
preparePreview(getDestination())
|
||||
true
|
||||
} else {
|
||||
|
@ -313,7 +305,7 @@ class ToolbarGestureHandler(
|
|||
val toolbarLocation = toolbarLayout.getRectWithViewLocation()
|
||||
// In Android 10, the system gesture touch area overlaps the bottom of the toolbar, so
|
||||
// lets make our swipe area taller by that amount
|
||||
windowInsets?.let { insets ->
|
||||
activity.window.decorView.getWindowInsets()?.let { insets ->
|
||||
if (activity.settings().shouldUseBottomToolbar) {
|
||||
toolbarLocation.top -= (insets.mandatorySystemGestureInsets.bottom - insets.stableInsetBottom)
|
||||
}
|
||||
|
|
|
@ -5,32 +5,11 @@
|
|||
package org.mozilla.fenix.components.metrics
|
||||
|
||||
import android.content.Context
|
||||
import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts
|
||||
import mozilla.components.browser.errorpages.ErrorType
|
||||
import mozilla.components.browser.menu.facts.BrowserMenuFacts
|
||||
import mozilla.components.browser.search.SearchEngine
|
||||
import mozilla.components.browser.toolbar.facts.ToolbarFacts
|
||||
import mozilla.components.concept.awesomebar.AwesomeBar
|
||||
import mozilla.components.feature.awesomebar.provider.BookmarksStorageSuggestionProvider
|
||||
import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
|
||||
import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider
|
||||
import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider
|
||||
import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
|
||||
import mozilla.components.feature.contextmenu.facts.ContextMenuFacts
|
||||
import mozilla.components.feature.customtabs.CustomTabsFacts
|
||||
import mozilla.components.feature.downloads.facts.DownloadsFacts
|
||||
import mozilla.components.feature.findinpage.facts.FindInPageFacts
|
||||
import mozilla.components.feature.media.facts.MediaFacts
|
||||
import mozilla.components.support.base.Component
|
||||
import mozilla.components.support.base.facts.Action
|
||||
import mozilla.components.support.base.facts.Fact
|
||||
import mozilla.components.support.base.facts.FactProcessor
|
||||
import mozilla.components.support.base.facts.Facts
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import mozilla.components.support.webextensions.facts.WebExtensionFacts
|
||||
import org.mozilla.fenix.BuildConfig
|
||||
import org.mozilla.fenix.GleanMetrics.Addons
|
||||
import org.mozilla.fenix.GleanMetrics.AppTheme
|
||||
import org.mozilla.fenix.GleanMetrics.Autoplay
|
||||
import org.mozilla.fenix.GleanMetrics.Collections
|
||||
import org.mozilla.fenix.GleanMetrics.ContextMenu
|
||||
import org.mozilla.fenix.GleanMetrics.CrashReporter
|
||||
|
@ -38,13 +17,12 @@ import org.mozilla.fenix.GleanMetrics.ErrorPage
|
|||
import org.mozilla.fenix.GleanMetrics.Events
|
||||
import org.mozilla.fenix.GleanMetrics.Logins
|
||||
import org.mozilla.fenix.GleanMetrics.Onboarding
|
||||
import org.mozilla.fenix.GleanMetrics.PerfAwesomebar
|
||||
import org.mozilla.fenix.GleanMetrics.ProgressiveWebApp
|
||||
import org.mozilla.fenix.GleanMetrics.SearchShortcuts
|
||||
import org.mozilla.fenix.GleanMetrics.Tip
|
||||
import org.mozilla.fenix.GleanMetrics.ToolbarSettings
|
||||
import org.mozilla.fenix.GleanMetrics.TrackingProtection
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.search.awesomebar.ShortcutsSuggestionProvider
|
||||
import java.util.Locale
|
||||
|
||||
sealed class Event {
|
||||
|
@ -176,12 +154,19 @@ sealed class Event {
|
|||
object SearchWidgetCFRCanceled : Event()
|
||||
object SearchWidgetCFRNotNowPressed : Event()
|
||||
object SearchWidgetCFRAddWidgetPressed : Event()
|
||||
object SearchWidgetInstalled : Event()
|
||||
object OnboardingAutoSignIn : Event()
|
||||
object OnboardingManualSignIn : Event()
|
||||
object OnboardingPrivacyNotice : Event()
|
||||
object OnboardingPrivateBrowsing : Event()
|
||||
object OnboardingWhatsNew : Event()
|
||||
object OnboardingFinish : Event()
|
||||
object ChangedToDefaultBrowser : Event()
|
||||
|
||||
object LoginDialogPromptDisplayed : Event()
|
||||
object LoginDialogPromptCancelled : Event()
|
||||
object LoginDialogPromptSave : Event()
|
||||
object LoginDialogPromptNeverSave : Event()
|
||||
|
||||
object ContextualHintETPDisplayed : Event()
|
||||
object ContextualHintETPDismissed : Event()
|
||||
|
@ -201,7 +186,21 @@ sealed class Event {
|
|||
object TabsTrayShareAllTabsPressed : Event()
|
||||
object TabsTrayCloseAllTabsPressed : Event()
|
||||
|
||||
object ProgressiveWebAppOpenFromHomescreenTap : Event()
|
||||
object ProgressiveWebAppInstallAsShortcut : Event()
|
||||
|
||||
// Interaction events with extras
|
||||
|
||||
data class ProgressiveWebAppForeground(val timeForegrounded: Long) : Event() {
|
||||
override val extras: Map<ProgressiveWebApp.foregroundKeys, String>?
|
||||
get() = mapOf(ProgressiveWebApp.foregroundKeys.timeMs to timeForegrounded.toString())
|
||||
}
|
||||
|
||||
data class ProgressiveWebAppBackground(val timeBackgrounded: Long) : Event() {
|
||||
override val extras: Map<ProgressiveWebApp.backgroundKeys, String>?
|
||||
get() = mapOf(ProgressiveWebApp.backgroundKeys.timeMs to timeBackgrounded.toString())
|
||||
}
|
||||
|
||||
data class OnboardingToolbarPosition(val position: Position) : Event() {
|
||||
enum class Position { TOP, BOTTOM }
|
||||
|
||||
|
@ -505,205 +504,19 @@ sealed class Event {
|
|||
get() = mapOf(Events.tabCounterMenuActionKeys.item to item.toString().toLowerCase(Locale.ROOT))
|
||||
}
|
||||
|
||||
object AutoPlaySettingVisited : Event()
|
||||
|
||||
data class AutoPlaySettingChanged(val setting: AutoplaySetting) : Event() {
|
||||
enum class AutoplaySetting {
|
||||
BLOCK_CELLULAR, BLOCK_AUDIO, BLOCK_ALL
|
||||
}
|
||||
|
||||
override val extras: Map<Autoplay.settingChangedKeys, String>?
|
||||
get() = mapOf(Autoplay.settingChangedKeys.autoplaySetting to setting.toString().toLowerCase(Locale.ROOT))
|
||||
}
|
||||
|
||||
sealed class Search
|
||||
|
||||
internal open val extras: Map<*, String>?
|
||||
get() = null
|
||||
}
|
||||
|
||||
private fun Fact.toEvent(): Event? = when (Pair(component, item)) {
|
||||
Component.FEATURE_FINDINPAGE to FindInPageFacts.Items.CLOSE -> Event.FindInPageClosed
|
||||
Component.FEATURE_FINDINPAGE to FindInPageFacts.Items.INPUT -> Event.FindInPageSearchCommitted
|
||||
Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.ITEM -> {
|
||||
metadata?.get("item")?.let { Event.ContextMenuItemTapped.create(it.toString()) }
|
||||
}
|
||||
|
||||
Component.BROWSER_TOOLBAR to ToolbarFacts.Items.MENU -> {
|
||||
metadata?.get("customTab")?.let { Event.CustomTabsMenuOpened }
|
||||
}
|
||||
Component.BROWSER_MENU to BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM -> {
|
||||
metadata?.get("id")?.let { Event.AddonsOpenInToolbarMenu(it.toString()) }
|
||||
}
|
||||
Component.FEATURE_CUSTOMTABS to CustomTabsFacts.Items.CLOSE -> Event.CustomTabsClosed
|
||||
Component.FEATURE_CUSTOMTABS to CustomTabsFacts.Items.ACTION_BUTTON -> Event.CustomTabsActionTapped
|
||||
|
||||
Component.FEATURE_DOWNLOADS to DownloadsFacts.Items.NOTIFICATION -> {
|
||||
when (action) {
|
||||
Action.CANCEL -> Event.NotificationDownloadCancel
|
||||
Action.OPEN -> Event.NotificationDownloadOpen
|
||||
Action.PAUSE -> Event.NotificationDownloadPause
|
||||
Action.RESUME -> Event.NotificationDownloadResume
|
||||
Action.TRY_AGAIN -> Event.NotificationDownloadTryAgain
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
Component.FEATURE_MEDIA to MediaFacts.Items.NOTIFICATION -> {
|
||||
when (action) {
|
||||
Action.PLAY -> Event.NotificationMediaPlay
|
||||
Action.PAUSE -> Event.NotificationMediaPause
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
Component.FEATURE_MEDIA to MediaFacts.Items.STATE -> {
|
||||
when (action) {
|
||||
Action.PLAY -> Event.MediaPlayState
|
||||
Action.PAUSE -> Event.MediaPauseState
|
||||
Action.STOP -> Event.MediaStopState
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
Component.SUPPORT_WEBEXTENSIONS to WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED -> {
|
||||
metadata?.get("installed")?.let { installedAddons ->
|
||||
if (installedAddons is List<*>) {
|
||||
Addons.installedAddons.set(installedAddons.map { it.toString() })
|
||||
Addons.hasInstalledAddons.set(installedAddons.size > 0)
|
||||
}
|
||||
}
|
||||
|
||||
metadata?.get("enabled")?.let { enabledAddons ->
|
||||
if (enabledAddons is List<*>) {
|
||||
Addons.enabledAddons.set(enabledAddons.map { it.toString() })
|
||||
Addons.hasEnabledAddons.set(enabledAddons.size > 0)
|
||||
}
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
Component.BROWSER_AWESOMEBAR to BrowserAwesomeBarFacts.Items.PROVIDER_DURATION -> {
|
||||
metadata?.get(BrowserAwesomeBarFacts.MetadataKeys.DURATION_PAIR)?.let { providerTiming ->
|
||||
require(providerTiming is Pair<*, *>) { "Expected providerTiming to be a Pair" }
|
||||
when (val provider = providerTiming.first as AwesomeBar.SuggestionProvider) {
|
||||
is HistoryStorageSuggestionProvider -> PerfAwesomebar.historySuggestions
|
||||
is BookmarksStorageSuggestionProvider -> PerfAwesomebar.bookmarkSuggestions
|
||||
is SessionSuggestionProvider -> PerfAwesomebar.sessionSuggestions
|
||||
is SearchSuggestionProvider -> PerfAwesomebar.searchEngineSuggestions
|
||||
is ClipboardSuggestionProvider -> PerfAwesomebar.clipboardSuggestions
|
||||
is ShortcutsSuggestionProvider -> PerfAwesomebar.shortcutsSuggestions
|
||||
// NB: add PerfAwesomebar.syncedTabsSuggestions once we're using SyncedTabsSuggestionProvider
|
||||
else -> {
|
||||
Logger("Metrics").error("Unknown suggestion provider: $provider")
|
||||
null
|
||||
}
|
||||
}?.accumulateSamples(longArrayOf(providerTiming.second as Long))
|
||||
}
|
||||
null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
enum class MetricServiceType {
|
||||
Data, Marketing;
|
||||
}
|
||||
|
||||
interface MetricsService {
|
||||
val type: MetricServiceType
|
||||
|
||||
fun start()
|
||||
fun stop()
|
||||
fun track(event: Event)
|
||||
fun shouldTrack(event: Event): Boolean
|
||||
}
|
||||
|
||||
interface MetricController {
|
||||
fun start(type: MetricServiceType)
|
||||
fun stop(type: MetricServiceType)
|
||||
fun track(event: Event)
|
||||
|
||||
companion object {
|
||||
fun create(
|
||||
services: List<MetricsService>,
|
||||
isDataTelemetryEnabled: () -> Boolean,
|
||||
isMarketingDataTelemetryEnabled: () -> Boolean
|
||||
): MetricController {
|
||||
return if (BuildConfig.TELEMETRY) {
|
||||
ReleaseMetricController(
|
||||
services,
|
||||
isDataTelemetryEnabled,
|
||||
isMarketingDataTelemetryEnabled
|
||||
)
|
||||
} else DebugMetricController()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DebugMetricController : MetricController {
|
||||
override fun start(type: MetricServiceType) {
|
||||
Logger.debug("DebugMetricController: start")
|
||||
}
|
||||
|
||||
override fun stop(type: MetricServiceType) {
|
||||
Logger.debug("DebugMetricController: stop")
|
||||
}
|
||||
|
||||
override fun track(event: Event) {
|
||||
Logger.debug("DebugMetricController: track event: $event")
|
||||
}
|
||||
}
|
||||
|
||||
private class ReleaseMetricController(
|
||||
private val services: List<MetricsService>,
|
||||
private val isDataTelemetryEnabled: () -> Boolean,
|
||||
private val isMarketingDataTelemetryEnabled: () -> Boolean
|
||||
) : MetricController {
|
||||
private var initialized = mutableSetOf<MetricServiceType>()
|
||||
|
||||
init {
|
||||
Facts.registerProcessor(object : FactProcessor {
|
||||
override fun process(fact: Fact) {
|
||||
fact.toEvent()?.also {
|
||||
track(it)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun start(type: MetricServiceType) {
|
||||
val isEnabled = isTelemetryEnabled(type)
|
||||
val isInitialized = isInitialized(type)
|
||||
if (!isEnabled || isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
services
|
||||
.filter { it.type == type }
|
||||
.forEach { it.start() }
|
||||
|
||||
initialized.add(type)
|
||||
}
|
||||
|
||||
override fun stop(type: MetricServiceType) {
|
||||
val isEnabled = isTelemetryEnabled(type)
|
||||
val isInitialized = isInitialized(type)
|
||||
if (isEnabled || !isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
services
|
||||
.filter { it.type == type }
|
||||
.forEach { it.stop() }
|
||||
|
||||
initialized.remove(type)
|
||||
}
|
||||
|
||||
override fun track(event: Event) {
|
||||
services
|
||||
.filter { it.shouldTrack(event) }
|
||||
.forEach {
|
||||
val isEnabled = isTelemetryEnabled(it.type)
|
||||
val isInitialized = isInitialized(it.type)
|
||||
if (!isEnabled || !isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
it.track(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInitialized(type: MetricServiceType): Boolean = initialized.contains(type)
|
||||
|
||||
private fun isTelemetryEnabled(type: MetricServiceType): Boolean = when (type) {
|
||||
MetricServiceType.Data -> isDataTelemetryEnabled()
|
||||
MetricServiceType.Marketing -> isMarketingDataTelemetryEnabled()
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import mozilla.components.support.base.log.logger.Logger
|
|||
import org.mozilla.fenix.GleanMetrics.AboutPage
|
||||
import org.mozilla.fenix.GleanMetrics.Addons
|
||||
import org.mozilla.fenix.GleanMetrics.AppTheme
|
||||
import org.mozilla.fenix.GleanMetrics.Autoplay
|
||||
import org.mozilla.fenix.GleanMetrics.BookmarksManagement
|
||||
import org.mozilla.fenix.GleanMetrics.BrowserSearch
|
||||
import org.mozilla.fenix.GleanMetrics.Collections
|
||||
|
@ -24,6 +25,7 @@ import org.mozilla.fenix.GleanMetrics.ErrorPage
|
|||
import org.mozilla.fenix.GleanMetrics.Events
|
||||
import org.mozilla.fenix.GleanMetrics.FindInPage
|
||||
import org.mozilla.fenix.GleanMetrics.History
|
||||
import org.mozilla.fenix.GleanMetrics.LoginDialog
|
||||
import org.mozilla.fenix.GleanMetrics.Logins
|
||||
import org.mozilla.fenix.GleanMetrics.MediaNotification
|
||||
import org.mozilla.fenix.GleanMetrics.MediaState
|
||||
|
@ -34,6 +36,7 @@ import org.mozilla.fenix.GleanMetrics.Pocket
|
|||
import org.mozilla.fenix.GleanMetrics.Preferences
|
||||
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingMode
|
||||
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut
|
||||
import org.mozilla.fenix.GleanMetrics.ProgressiveWebApp
|
||||
import org.mozilla.fenix.GleanMetrics.QrScanner
|
||||
import org.mozilla.fenix.GleanMetrics.ReaderMode
|
||||
import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine
|
||||
|
@ -44,6 +47,7 @@ import org.mozilla.fenix.GleanMetrics.SearchWidgetCfr
|
|||
import org.mozilla.fenix.GleanMetrics.SyncAccount
|
||||
import org.mozilla.fenix.GleanMetrics.SyncAuth
|
||||
import org.mozilla.fenix.GleanMetrics.Tab
|
||||
import org.mozilla.fenix.GleanMetrics.TabsTray
|
||||
import org.mozilla.fenix.GleanMetrics.Tip
|
||||
import org.mozilla.fenix.GleanMetrics.ToolbarSettings
|
||||
import org.mozilla.fenix.GleanMetrics.TopSites
|
||||
|
@ -140,6 +144,18 @@ private val Event.wrapper: EventWrapper<*>?
|
|||
{ SearchShortcuts.selected.record(it) },
|
||||
{ SearchShortcuts.selectedKeys.valueOf(it) }
|
||||
)
|
||||
is Event.LoginDialogPromptDisplayed -> EventWrapper<NoExtraKeys>(
|
||||
{ LoginDialog.displayed.record(it) }
|
||||
)
|
||||
is Event.LoginDialogPromptCancelled -> EventWrapper<NoExtraKeys>(
|
||||
{ LoginDialog.cancelled.record(it) }
|
||||
)
|
||||
is Event.LoginDialogPromptSave -> EventWrapper<NoExtraKeys>(
|
||||
{ LoginDialog.saved.record(it) }
|
||||
)
|
||||
is Event.LoginDialogPromptNeverSave -> EventWrapper<NoExtraKeys>(
|
||||
{ LoginDialog.neverSave.record(it) }
|
||||
)
|
||||
is Event.FindInPageOpened -> EventWrapper<NoExtraKeys>(
|
||||
{ FindInPage.opened.record(it) }
|
||||
)
|
||||
|
@ -620,40 +636,61 @@ private val Event.wrapper: EventWrapper<*>?
|
|||
)
|
||||
|
||||
is Event.TabsTrayOpened -> EventWrapper<NoExtraKeys>(
|
||||
{ org.mozilla.fenix.GleanMetrics.TabsTray.opened.record(it) }
|
||||
{ TabsTray.opened.record(it) }
|
||||
)
|
||||
is Event.TabsTrayClosed -> EventWrapper<NoExtraKeys>(
|
||||
{ org.mozilla.fenix.GleanMetrics.TabsTray.closed.record(it) }
|
||||
{ TabsTray.closed.record(it) }
|
||||
)
|
||||
is Event.OpenedExistingTab -> EventWrapper<NoExtraKeys>(
|
||||
{ org.mozilla.fenix.GleanMetrics.TabsTray.openedExistingTab.record(it) }
|
||||
{ TabsTray.openedExistingTab.record(it) }
|
||||
)
|
||||
is Event.ClosedExistingTab -> EventWrapper<NoExtraKeys>(
|
||||
{ org.mozilla.fenix.GleanMetrics.TabsTray.closedExistingTab.record(it) }
|
||||
{ TabsTray.closedExistingTab.record(it) }
|
||||
)
|
||||
is Event.TabsTrayPrivateModeTapped -> EventWrapper<NoExtraKeys>(
|
||||
{ org.mozilla.fenix.GleanMetrics.TabsTray.privateModeTapped.record(it) }
|
||||
{ TabsTray.privateModeTapped.record(it) }
|
||||
)
|
||||
is Event.TabsTrayNormalModeTapped -> EventWrapper<NoExtraKeys>(
|
||||
{ org.mozilla.fenix.GleanMetrics.TabsTray.normalModeTapped.record(it) }
|
||||
{ TabsTray.normalModeTapped.record(it) }
|
||||
)
|
||||
is Event.NewTabTapped -> EventWrapper<NoExtraKeys>(
|
||||
{ org.mozilla.fenix.GleanMetrics.TabsTray.newTabTapped.record(it) }
|
||||
{ TabsTray.newTabTapped.record(it) }
|
||||
)
|
||||
is Event.NewPrivateTabTapped -> EventWrapper<NoExtraKeys>(
|
||||
{ org.mozilla.fenix.GleanMetrics.TabsTray.newPrivateTabTapped.record(it) }
|
||||
{ TabsTray.newPrivateTabTapped.record(it) }
|
||||
)
|
||||
is Event.TabsTrayMenuOpened -> EventWrapper<NoExtraKeys>(
|
||||
{ org.mozilla.fenix.GleanMetrics.TabsTray.menuOpened.record(it) }
|
||||
{ TabsTray.menuOpened.record(it) }
|
||||
)
|
||||
is Event.TabsTraySaveToCollectionPressed -> EventWrapper<NoExtraKeys>(
|
||||
{ org.mozilla.fenix.GleanMetrics.TabsTray.saveToCollection.record(it) }
|
||||
{ TabsTray.saveToCollection.record(it) }
|
||||
)
|
||||
is Event.TabsTrayShareAllTabsPressed -> EventWrapper<NoExtraKeys>(
|
||||
{ org.mozilla.fenix.GleanMetrics.TabsTray.shareAllTabs.record(it) }
|
||||
{ TabsTray.shareAllTabs.record(it) }
|
||||
)
|
||||
is Event.TabsTrayCloseAllTabsPressed -> EventWrapper<NoExtraKeys>(
|
||||
{ org.mozilla.fenix.GleanMetrics.TabsTray.closeAllTabs.record(it) }
|
||||
{ TabsTray.closeAllTabs.record(it) }
|
||||
)
|
||||
Event.AutoPlaySettingVisited -> EventWrapper<NoExtraKeys>(
|
||||
{ Autoplay.visitedSetting.record(it) }
|
||||
)
|
||||
is Event.AutoPlaySettingChanged -> EventWrapper(
|
||||
{ Autoplay.settingChanged.record(it) },
|
||||
{ Autoplay.settingChangedKeys.valueOf(it) }
|
||||
)
|
||||
is Event.ProgressiveWebAppOpenFromHomescreenTap -> EventWrapper<NoExtraKeys>(
|
||||
{ ProgressiveWebApp.homescreenTap.record(it) }
|
||||
)
|
||||
is Event.ProgressiveWebAppInstallAsShortcut -> EventWrapper<NoExtraKeys>(
|
||||
{ ProgressiveWebApp.installTap.record(it) }
|
||||
)
|
||||
is Event.ProgressiveWebAppForeground -> EventWrapper(
|
||||
{ ProgressiveWebApp.foreground.record(it) },
|
||||
{ ProgressiveWebApp.foregroundKeys.valueOf(it) }
|
||||
)
|
||||
is Event.ProgressiveWebAppBackground -> EventWrapper(
|
||||
{ ProgressiveWebApp.background.record(it) },
|
||||
{ ProgressiveWebApp.backgroundKeys.valueOf(it) }
|
||||
)
|
||||
|
||||
// Don't record other events in Glean:
|
||||
|
@ -665,6 +702,8 @@ private val Event.wrapper: EventWrapper<*>?
|
|||
is Event.DismissedOnboarding -> null
|
||||
is Event.FennecToFenixMigrated -> null
|
||||
is Event.AddonInstalled -> null
|
||||
is Event.SearchWidgetInstalled -> null
|
||||
is Event.ChangedToDefaultBrowser -> null
|
||||
}
|
||||
|
||||
class GleanMetricsService(private val context: Context) : MetricsService {
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.mozilla.fenix.BuildConfig
|
|||
import org.mozilla.fenix.components.metrics.MozillaProductDetector.MozillaProducts
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import java.util.Locale
|
||||
import java.util.MissingResourceException
|
||||
import java.util.UUID.randomUUID
|
||||
|
||||
private val Event.name: String?
|
||||
|
@ -39,6 +40,9 @@ private val Event.name: String?
|
|||
is Event.DismissedOnboarding -> "E_Dismissed_Onboarding"
|
||||
is Event.FennecToFenixMigrated -> "E_Fennec_To_Fenix_Migrated"
|
||||
is Event.AddonInstalled -> "E_Addon_Installed"
|
||||
is Event.SearchWidgetInstalled -> "E_Search_Widget_Added"
|
||||
is Event.ChangedToDefaultBrowser -> "E_Changed_Default_To_Fenix"
|
||||
is Event.TrackingProtectionSettingChanged -> "E_Changed_ETP"
|
||||
|
||||
// Do not track other events in Leanplum
|
||||
else -> null
|
||||
|
@ -80,12 +84,19 @@ class LeanplumMetricsService(private val application: Application) : MetricsServ
|
|||
leanplumJob = scope.launch {
|
||||
|
||||
val applicationSetLocale = LocaleManager.getCurrentLocale(application)
|
||||
val currentLocale = when (applicationSetLocale != null) {
|
||||
true -> applicationSetLocale.isO3Language
|
||||
false -> Locale.getDefault().isO3Language
|
||||
}
|
||||
if (!isLeanplumEnabled(currentLocale)) {
|
||||
Log.i(LOGTAG, "Leanplum is not available for this locale: $currentLocale")
|
||||
val currentLocale = applicationSetLocale ?: Locale.getDefault()
|
||||
val languageCode =
|
||||
currentLocale.iso3LanguageOrNull
|
||||
?: currentLocale.language.let {
|
||||
if (it.isNotBlank()) {
|
||||
it
|
||||
} else {
|
||||
currentLocale.toString()
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLeanplumEnabled(languageCode)) {
|
||||
Log.i(LOGTAG, "Leanplum is not available for this locale: $languageCode")
|
||||
return@launch
|
||||
}
|
||||
|
||||
|
@ -167,6 +178,12 @@ class LeanplumMetricsService(private val application: Application) : MetricsServ
|
|||
return LEANPLUM_ENABLED_LOCALES.contains(locale)
|
||||
}
|
||||
|
||||
private val Locale.iso3LanguageOrNull: String?
|
||||
get() =
|
||||
try {
|
||||
this.isO3Language
|
||||
} catch (_: MissingResourceException) { null }
|
||||
|
||||
companion object {
|
||||
private const val LOGTAG = "LeanplumMetricsService"
|
||||
|
||||
|
@ -178,7 +195,7 @@ class LeanplumMetricsService(private val application: Application) : MetricsServ
|
|||
get() = BuildConfig.LEANPLUM_TOKEN.orEmpty()
|
||||
// Leanplum needs to be enabled for the following locales.
|
||||
// Irrespective of the actual device location.
|
||||
private val LEANPLUM_ENABLED_LOCALES = listOf(
|
||||
private val LEANPLUM_ENABLED_LOCALES = setOf(
|
||||
"eng", // English
|
||||
"zho", // Chinese
|
||||
"deu", // German
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -6,6 +6,7 @@ package org.mozilla.fenix.customtabs
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.SystemClock
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.navigation.fragment.navArgs
|
||||
|
@ -30,7 +31,9 @@ import org.mozilla.fenix.R
|
|||
import org.mozilla.fenix.browser.BaseBrowserFragment
|
||||
import org.mozilla.fenix.browser.CustomTabContextMenuCandidate
|
||||
import org.mozilla.fenix.browser.FenixSnackbarDelegate
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.metrics
|
||||
import org.mozilla.fenix.ext.nav
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.settings
|
||||
|
@ -150,6 +153,22 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
|
|||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val currTimeMs = SystemClock.elapsedRealtimeNanos() / MS_PRECISION
|
||||
requireComponents.analytics.metrics.track(
|
||||
Event.ProgressiveWebAppForeground(currTimeMs)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
val currTimeMs = SystemClock.elapsedRealtimeNanos() / MS_PRECISION
|
||||
requireComponents.analytics.metrics.track(
|
||||
Event.ProgressiveWebAppBackground(currTimeMs)
|
||||
)
|
||||
}
|
||||
|
||||
override fun removeSessionIfNeeded(): Boolean {
|
||||
return customTabsIntegration.onBackPressed() || super.removeSessionIfNeeded()
|
||||
}
|
||||
|
@ -192,4 +211,9 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
|
|||
view,
|
||||
FenixSnackbarDelegate(view)
|
||||
)
|
||||
|
||||
companion object {
|
||||
// We only care about millisecond precision for telemetry events
|
||||
internal const val MS_PRECISION = 1_000_000L
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.loginexceptions
|
||||
package org.mozilla.fenix.exceptions.login
|
||||
|
||||
import mozilla.components.feature.logins.exceptions.LoginException
|
||||
import mozilla.components.lib.state.Action
|
||||
|
@ -26,7 +26,7 @@ sealed class ExceptionsFragmentAction : Action {
|
|||
* The state for the Exceptions Screen
|
||||
* @property items List of exceptions to display
|
||||
*/
|
||||
data class ExceptionsFragmentState(val items: List<LoginException>) : State
|
||||
data class ExceptionsFragmentState(val items: List<LoginException> = emptyList()) : State
|
||||
|
||||
/**
|
||||
* The ExceptionsState Reducer.
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.loginexceptions
|
||||
package org.mozilla.fenix.exceptions.login
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
|
@ -13,10 +13,9 @@ import androidx.lifecycle.asLiveData
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.observe
|
||||
import kotlinx.android.synthetic.main.fragment_exceptions.view.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.feature.logins.exceptions.LoginException
|
||||
import kotlinx.coroutines.plus
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
|
@ -45,14 +44,17 @@ class LoginExceptionsFragment : Fragment() {
|
|||
val view = inflater.inflate(R.layout.fragment_exceptions, container, false)
|
||||
exceptionsStore = StoreProvider.get(this) {
|
||||
ExceptionsFragmentStore(
|
||||
ExceptionsFragmentState(
|
||||
items = listOf()
|
||||
)
|
||||
ExceptionsFragmentState(items = emptyList())
|
||||
)
|
||||
}
|
||||
exceptionsInteractor =
|
||||
LoginExceptionsInteractor(::deleteOneItem, ::deleteAllItems)
|
||||
exceptionsView = LoginExceptionsView(view.exceptionsLayout, exceptionsInteractor)
|
||||
exceptionsInteractor = DefaultLoginExceptionsInteractor(
|
||||
ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO,
|
||||
loginExceptionStorage = requireComponents.core.loginExceptionStorage
|
||||
)
|
||||
exceptionsView = LoginExceptionsView(
|
||||
view.exceptionsLayout,
|
||||
exceptionsInteractor
|
||||
)
|
||||
subscribeToLoginExceptions()
|
||||
return view
|
||||
}
|
||||
|
@ -67,19 +69,7 @@ class LoginExceptionsFragment : Fragment() {
|
|||
@ExperimentalCoroutinesApi
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
consumeFrom(exceptionsStore) {
|
||||
exceptionsView.update(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteAllItems() {
|
||||
viewLifecycleOwner.lifecycleScope.launch(IO) {
|
||||
requireComponents.core.loginExceptionStorage.deleteAllLoginExceptions()
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteOneItem(item: LoginException) {
|
||||
viewLifecycleOwner.lifecycleScope.launch(IO) {
|
||||
requireComponents.core.loginExceptionStorage.removeLoginException(item)
|
||||
exceptionsView.update(it.items)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,19 +2,13 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.trackingprotectionexceptions
|
||||
package org.mozilla.fenix.exceptions.trackingprotection
|
||||
|
||||
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
|
||||
import mozilla.components.lib.state.Action
|
||||
import mozilla.components.lib.state.State
|
||||
import mozilla.components.lib.state.Store
|
||||
|
||||
/**
|
||||
* Class representing an exception item
|
||||
* @property url Host of the exception
|
||||
*/
|
||||
data class ExceptionItem(override val url: String) : TrackingProtectionException
|
||||
|
||||
/**
|
||||
* The [Store] for holding the [ExceptionsFragmentState] and applying [ExceptionsFragmentAction]s.
|
||||
*/
|
||||
|
@ -32,7 +26,7 @@ sealed class ExceptionsFragmentAction : Action {
|
|||
* The state for the Exceptions Screen
|
||||
* @property items List of exceptions to display
|
||||
*/
|
||||
data class ExceptionsFragmentState(val items: List<TrackingProtectionException>) : State
|
||||
data class ExceptionsFragmentState(val items: List<TrackingProtectionException> = emptyList()) : State
|
||||
|
||||
/**
|
||||
* The ExceptionsState Reducer.
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,27 +2,23 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.trackingprotectionexceptions.viewholders
|
||||
package org.mozilla.fenix.exceptions.viewholders
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.delete_exceptions_button.view.*
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsInteractor
|
||||
import org.mozilla.fenix.exceptions.ExceptionsInteractor
|
||||
|
||||
class ExceptionsDeleteButtonViewHolder(
|
||||
view: View,
|
||||
private val interactor: ExceptionsInteractor
|
||||
private val interactor: ExceptionsInteractor<*>
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
private val deleteButton = view.removeAllExceptions
|
||||
|
||||
init {
|
||||
val deleteButton: MaterialButton = view.findViewById(R.id.removeAllExceptions)
|
||||
deleteButton.setOnClickListener {
|
||||
interactor.onDeleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LAYOUT_ID = R.layout.delete_exceptions_button
|
||||
}
|
||||
}
|
|
@ -2,20 +2,21 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.loginexceptions.viewholders
|
||||
package org.mozilla.fenix.exceptions.viewholders
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.exceptions_description.view.*
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
class LoginExceptionsHeaderViewHolder(
|
||||
view: View
|
||||
class ExceptionsHeaderViewHolder(
|
||||
view: View,
|
||||
@StringRes description: Int
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
init {
|
||||
view.exceptions_description.text =
|
||||
view.context.getString(R.string.preferences_passwords_exceptions_description)
|
||||
view.exceptions_description.text = view.context.getString(description)
|
||||
}
|
||||
|
||||
companion object {
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -5,8 +5,12 @@
|
|||
package org.mozilla.fenix.ext
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.view.TouchDelegate
|
||||
import android.view.View
|
||||
import androidx.annotation.Dimension
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import mozilla.components.support.ktx.android.util.dpToPx
|
||||
|
||||
fun View.increaseTapArea(extraDps: Int) {
|
||||
|
@ -26,3 +30,61 @@ fun View.removeTouchDelegate() {
|
|||
parent.touchDelegate = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A safer version of [ViewCompat.getRootWindowInsets] that does not throw a NullPointerException
|
||||
* if the view is not attached.
|
||||
*/
|
||||
fun View.getWindowInsets(): WindowInsetsCompat? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
rootWindowInsets?.let {
|
||||
WindowInsetsCompat.toWindowInsetsCompat(it)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the keyboard is visible
|
||||
*
|
||||
* Inspired by https://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
|
||||
* API 30 adds a native method for this. We should use it (and a compat method if one
|
||||
* is added) when it becomes available
|
||||
*/
|
||||
fun View.isKeyboardVisible(): Boolean {
|
||||
// Since we have insets in M and above, we don't need to guess what the keyboard height is.
|
||||
// Otherwise, we make a guess at the minimum height of the keyboard to account for the
|
||||
// navigation bar.
|
||||
val minimumKeyboardHeight = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
0
|
||||
} else {
|
||||
MINIMUM_KEYBOARD_HEIGHT.dpToPx(resources.displayMetrics)
|
||||
}
|
||||
return getKeyboardHeight() > minimumKeyboardHeight
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun View.getWindowVisibleDisplayFrame(): Rect = with(Rect()) {
|
||||
getWindowVisibleDisplayFrame(this)
|
||||
this
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun View.getKeyboardHeight(): Int {
|
||||
val windowRect = getWindowVisibleDisplayFrame()
|
||||
val statusBarHeight = windowRect.top
|
||||
var keyboardHeight = rootView.height - (windowRect.height() + statusBarHeight)
|
||||
getWindowInsets()?.let {
|
||||
keyboardHeight -= it.stableInsetBottom
|
||||
}
|
||||
|
||||
return keyboardHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* The assumed minimum height of the keyboard.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
@Dimension(unit = Dimension.DP)
|
||||
internal const val MINIMUM_KEYBOARD_HEIGHT = 100
|
||||
|
|
|
@ -211,7 +211,8 @@ class HomeFragment : Fragment() {
|
|||
hideOnboarding = ::hideOnboardingAndOpenSearch,
|
||||
registerCollectionStorageObserver = ::registerCollectionStorageObserver,
|
||||
showDeleteCollectionPrompt = ::showDeleteCollectionPrompt,
|
||||
showTabTray = ::openTabTray
|
||||
showTabTray = ::openTabTray,
|
||||
handleSwipedItemDeletionCancel = ::handleSwipedItemDeletionCancel
|
||||
)
|
||||
)
|
||||
updateLayout(view)
|
||||
|
@ -557,12 +558,21 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun showDeleteCollectionPrompt(tabCollection: TabCollection, title: String?, message: String) {
|
||||
private fun showDeleteCollectionPrompt(
|
||||
tabCollection: TabCollection,
|
||||
title: String?,
|
||||
message: String,
|
||||
wasSwiped: Boolean,
|
||||
handleSwipedItemDeletionCancel: () -> Unit
|
||||
) {
|
||||
val context = context ?: return
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(title)
|
||||
setMessage(message)
|
||||
setNegativeButton(R.string.tab_collection_dialog_negative) { dialog: DialogInterface, _ ->
|
||||
if (wasSwiped) {
|
||||
handleSwipedItemDeletionCancel()
|
||||
}
|
||||
dialog.cancel()
|
||||
}
|
||||
setPositiveButton(R.string.tab_collection_dialog_positive) { dialog: DialogInterface, _ ->
|
||||
|
@ -951,6 +961,10 @@ class HomeFragment : Fragment() {
|
|||
view?.add_tabs_to_collections_button?.isVisible = tabCount > 0
|
||||
}
|
||||
|
||||
private fun handleSwipedItemDeletionCancel() {
|
||||
view?.sessionControlRecyclerView?.adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ALL_NORMAL_TABS = "all_normal"
|
||||
const val ALL_PRIVATE_TABS = "all_private"
|
||||
|
|
|
@ -15,7 +15,6 @@ import mozilla.components.feature.tab.collections.TabCollection
|
|||
import mozilla.components.feature.top.sites.TopSite
|
||||
import org.mozilla.fenix.components.tips.Tip
|
||||
import org.mozilla.fenix.home.OnboardingState
|
||||
import org.mozilla.fenix.home.Tab
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder
|
||||
|
@ -38,8 +37,24 @@ import mozilla.components.feature.tab.collections.Tab as ComponentTab
|
|||
|
||||
sealed class AdapterItem(@LayoutRes val viewType: Int) {
|
||||
data class TipItem(val tip: Tip) : AdapterItem(
|
||||
ButtonTipViewHolder.LAYOUT_ID)
|
||||
data class TopSiteList(val topSites: List<TopSite>) : AdapterItem(TopSiteViewHolder.LAYOUT_ID)
|
||||
ButtonTipViewHolder.LAYOUT_ID
|
||||
)
|
||||
|
||||
data class TopSiteList(val topSites: List<TopSite>) : AdapterItem(TopSiteViewHolder.LAYOUT_ID) {
|
||||
override fun sameAs(other: AdapterItem): Boolean {
|
||||
val newTopSites = (other as? TopSiteList) ?: return false
|
||||
return newTopSites.topSites == this.topSites
|
||||
}
|
||||
|
||||
override fun contentsSameAs(other: AdapterItem): Boolean {
|
||||
val newTopSites = (other as? TopSiteList) ?: return false
|
||||
if (newTopSites.topSites.size != this.topSites.size) return false
|
||||
val newSitesSequence = newTopSites.topSites.asSequence()
|
||||
val oldTopSites = this.topSites.asSequence()
|
||||
return newSitesSequence.zip(oldTopSites).all { (new, old) -> new.title == old.title }
|
||||
}
|
||||
}
|
||||
|
||||
object PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID)
|
||||
object NoCollectionsMessage : AdapterItem(NoCollectionsMessageViewHolder.LAYOUT_ID)
|
||||
|
||||
|
@ -48,32 +63,48 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
|
|||
val collection: TabCollection,
|
||||
val expanded: Boolean
|
||||
) : AdapterItem(CollectionViewHolder.LAYOUT_ID) {
|
||||
override fun sameAs(other: AdapterItem) = other is CollectionItem && collection.id == other.collection.id
|
||||
override fun sameAs(other: AdapterItem) =
|
||||
other is CollectionItem && collection.id == other.collection.id
|
||||
|
||||
override fun contentsSameAs(other: AdapterItem): Boolean {
|
||||
(other as? CollectionItem)?.let {
|
||||
return it.expanded == this.expanded && it.collection.title == this.collection.title
|
||||
} ?: return false
|
||||
}
|
||||
}
|
||||
|
||||
data class TabInCollectionItem(
|
||||
val collection: TabCollection,
|
||||
val tab: ComponentTab,
|
||||
val isLastTab: Boolean
|
||||
) : AdapterItem(TabInCollectionViewHolder.LAYOUT_ID) {
|
||||
override fun sameAs(other: AdapterItem) = other is TabInCollectionItem && tab.id == other.tab.id
|
||||
override fun sameAs(other: AdapterItem) =
|
||||
other is TabInCollectionItem && tab.id == other.tab.id
|
||||
}
|
||||
|
||||
object OnboardingHeader : AdapterItem(OnboardingHeaderViewHolder.LAYOUT_ID)
|
||||
data class OnboardingSectionHeader(
|
||||
val labelBuilder: (Context) -> String
|
||||
) : AdapterItem(OnboardingSectionHeaderViewHolder.LAYOUT_ID) {
|
||||
override fun sameAs(other: AdapterItem) = other is OnboardingSectionHeader && labelBuilder == other.labelBuilder
|
||||
override fun sameAs(other: AdapterItem) =
|
||||
other is OnboardingSectionHeader && labelBuilder == other.labelBuilder
|
||||
}
|
||||
|
||||
object OnboardingManualSignIn : AdapterItem(OnboardingManualSignInViewHolder.LAYOUT_ID)
|
||||
data class OnboardingAutomaticSignIn(
|
||||
val state: OnboardingState.SignedOutCanAutoSignIn
|
||||
) : AdapterItem(OnboardingAutomaticSignInViewHolder.LAYOUT_ID)
|
||||
|
||||
object OnboardingThemePicker : AdapterItem(OnboardingThemePickerViewHolder.LAYOUT_ID)
|
||||
object OnboardingTrackingProtection : AdapterItem(OnboardingTrackingProtectionViewHolder.LAYOUT_ID)
|
||||
object OnboardingTrackingProtection :
|
||||
AdapterItem(OnboardingTrackingProtectionViewHolder.LAYOUT_ID)
|
||||
|
||||
object OnboardingPrivateBrowsing : AdapterItem(OnboardingPrivateBrowsingViewHolder.LAYOUT_ID)
|
||||
object OnboardingPrivacyNotice : AdapterItem(OnboardingPrivacyNoticeViewHolder.LAYOUT_ID)
|
||||
object OnboardingFinish : AdapterItem(OnboardingFinishViewHolder.LAYOUT_ID)
|
||||
object OnboardingToolbarPositionPicker : AdapterItem(OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID)
|
||||
object OnboardingToolbarPositionPicker :
|
||||
AdapterItem(OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID)
|
||||
|
||||
object OnboardingWhatsNew : AdapterItem(OnboardingWhatsNewViewHolder.LAYOUT_ID)
|
||||
|
||||
/**
|
||||
|
@ -85,26 +116,21 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
|
|||
* Returns a payload if there's been a change, or null if not
|
||||
*/
|
||||
open fun getChangePayload(newItem: AdapterItem): Any? = null
|
||||
|
||||
open fun contentsSameAs(other: AdapterItem) = this::class == other::class
|
||||
}
|
||||
|
||||
class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
|
||||
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = oldItem.sameAs(newItem)
|
||||
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
|
||||
oldItem.sameAs(newItem)
|
||||
|
||||
@Suppress("DiffUtilEquals")
|
||||
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = oldItem == newItem
|
||||
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
|
||||
oldItem.contentsSameAs(newItem)
|
||||
|
||||
override fun getChangePayload(oldItem: AdapterItem, newItem: AdapterItem): Any? {
|
||||
return oldItem.getChangePayload(newItem) ?: return super.getChangePayload(oldItem, newItem)
|
||||
}
|
||||
|
||||
data class TabChangePayload(
|
||||
val tab: Tab,
|
||||
val shouldUpdateFavicon: Boolean,
|
||||
val shouldUpdateHostname: Boolean,
|
||||
val shouldUpdateTitle: Boolean,
|
||||
val shouldUpdateSelected: Boolean,
|
||||
val shouldUpdateMediaState: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
class SessionControlAdapter(
|
||||
|
@ -119,23 +145,42 @@ class SessionControlAdapter(
|
|||
return when (viewType) {
|
||||
ButtonTipViewHolder.LAYOUT_ID -> ButtonTipViewHolder(view, interactor)
|
||||
TopSiteViewHolder.LAYOUT_ID -> TopSiteViewHolder(view, interactor)
|
||||
PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(view, interactor)
|
||||
PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(
|
||||
view,
|
||||
interactor
|
||||
)
|
||||
NoCollectionsMessageViewHolder.LAYOUT_ID ->
|
||||
NoCollectionsMessageViewHolder(view, interactor, hasNormalTabsOpened)
|
||||
CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view)
|
||||
CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, interactor)
|
||||
TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder(view, interactor, differentLastItem = true)
|
||||
TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder(
|
||||
view,
|
||||
interactor,
|
||||
differentLastItem = true
|
||||
)
|
||||
OnboardingHeaderViewHolder.LAYOUT_ID -> OnboardingHeaderViewHolder(view)
|
||||
OnboardingSectionHeaderViewHolder.LAYOUT_ID -> OnboardingSectionHeaderViewHolder(view)
|
||||
OnboardingAutomaticSignInViewHolder.LAYOUT_ID -> OnboardingAutomaticSignInViewHolder(view)
|
||||
OnboardingAutomaticSignInViewHolder.LAYOUT_ID -> OnboardingAutomaticSignInViewHolder(
|
||||
view
|
||||
)
|
||||
OnboardingManualSignInViewHolder.LAYOUT_ID -> OnboardingManualSignInViewHolder(view)
|
||||
OnboardingThemePickerViewHolder.LAYOUT_ID -> OnboardingThemePickerViewHolder(view)
|
||||
OnboardingTrackingProtectionViewHolder.LAYOUT_ID -> OnboardingTrackingProtectionViewHolder(view)
|
||||
OnboardingPrivateBrowsingViewHolder.LAYOUT_ID -> OnboardingPrivateBrowsingViewHolder(view, interactor)
|
||||
OnboardingPrivacyNoticeViewHolder.LAYOUT_ID -> OnboardingPrivacyNoticeViewHolder(view, interactor)
|
||||
OnboardingTrackingProtectionViewHolder.LAYOUT_ID -> OnboardingTrackingProtectionViewHolder(
|
||||
view
|
||||
)
|
||||
OnboardingPrivateBrowsingViewHolder.LAYOUT_ID -> OnboardingPrivateBrowsingViewHolder(
|
||||
view,
|
||||
interactor
|
||||
)
|
||||
OnboardingPrivacyNoticeViewHolder.LAYOUT_ID -> OnboardingPrivacyNoticeViewHolder(
|
||||
view,
|
||||
interactor
|
||||
)
|
||||
OnboardingFinishViewHolder.LAYOUT_ID -> OnboardingFinishViewHolder(view, interactor)
|
||||
OnboardingWhatsNewViewHolder.LAYOUT_ID -> OnboardingWhatsNewViewHolder(view, interactor)
|
||||
OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder(view)
|
||||
OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder(
|
||||
view
|
||||
)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ interface SessionControlController {
|
|||
/**
|
||||
* @see [CollectionInteractor.onCollectionRemoveTab]
|
||||
*/
|
||||
fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab)
|
||||
fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab, wasSwiped: Boolean)
|
||||
|
||||
/**
|
||||
* @see [CollectionInteractor.onCollectionShareTabsClicked]
|
||||
|
@ -160,8 +160,15 @@ class DefaultSessionControlController(
|
|||
private val viewLifecycleScope: CoroutineScope,
|
||||
private val hideOnboarding: () -> Unit,
|
||||
private val registerCollectionStorageObserver: () -> Unit,
|
||||
private val showDeleteCollectionPrompt: (tabCollection: TabCollection, title: String?, message: String) -> Unit,
|
||||
private val showTabTray: () -> Unit
|
||||
private val showDeleteCollectionPrompt: (
|
||||
tabCollection: TabCollection,
|
||||
title: String?,
|
||||
message: String,
|
||||
wasSwiped: Boolean,
|
||||
handleSwipedItemDeletionCancel: () -> Unit
|
||||
) -> Unit,
|
||||
private val showTabTray: () -> Unit,
|
||||
private val handleSwipedItemDeletionCancel: () -> Unit
|
||||
) : SessionControlController {
|
||||
|
||||
override fun handleCollectionAddTabTapped(collection: TabCollection) {
|
||||
|
@ -206,7 +213,7 @@ class DefaultSessionControlController(
|
|||
metrics.track(Event.CollectionAllTabsRestored)
|
||||
}
|
||||
|
||||
override fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab) {
|
||||
override fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab, wasSwiped: Boolean) {
|
||||
metrics.track(Event.CollectionTabRemoved)
|
||||
|
||||
if (collection.tabs.size == 1) {
|
||||
|
@ -216,7 +223,7 @@ class DefaultSessionControlController(
|
|||
)
|
||||
val message =
|
||||
activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
|
||||
showDeleteCollectionPrompt(collection, title, message)
|
||||
showDeleteCollectionPrompt(collection, title, message, wasSwiped, handleSwipedItemDeletionCancel)
|
||||
} else {
|
||||
viewLifecycleScope.launch(Dispatchers.IO) {
|
||||
tabCollectionStorage.removeTabFromCollection(collection, tab)
|
||||
|
@ -232,7 +239,7 @@ class DefaultSessionControlController(
|
|||
override fun handleDeleteCollectionTapped(collection: TabCollection) {
|
||||
val message =
|
||||
activity.resources.getString(R.string.tab_collection_dialog_message, collection.title)
|
||||
showDeleteCollectionPrompt(collection, null, message)
|
||||
showDeleteCollectionPrompt(collection, null, message, false, handleSwipedItemDeletionCancel)
|
||||
}
|
||||
|
||||
override fun handleOpenInPrivateTabClicked(topSite: TopSite) {
|
||||
|
|
|
@ -54,7 +54,7 @@ interface CollectionInteractor {
|
|||
* @param collection The collection of tabs that will be modified.
|
||||
* @param tab The tab to remove from the tab collection.
|
||||
*/
|
||||
fun onCollectionRemoveTab(collection: TabCollection, tab: Tab)
|
||||
fun onCollectionRemoveTab(collection: TabCollection, tab: Tab, wasSwiped: Boolean)
|
||||
|
||||
/**
|
||||
* Shares the tabs in the given tab collection. Called when a user clicks on the Collection
|
||||
|
@ -189,8 +189,8 @@ class SessionControlInteractor(
|
|||
controller.handleCollectionOpenTabsTapped(collection)
|
||||
}
|
||||
|
||||
override fun onCollectionRemoveTab(collection: TabCollection, tab: Tab) {
|
||||
controller.handleCollectionRemoveTab(collection, tab)
|
||||
override fun onCollectionRemoveTab(collection: TabCollection, tab: Tab, wasSwiped: Boolean) {
|
||||
controller.handleCollectionRemoveTab(collection, tab, wasSwiped)
|
||||
}
|
||||
|
||||
override fun onCollectionShareTabsClicked(collection: TabCollection) {
|
||||
|
|
|
@ -29,7 +29,7 @@ class SwipeToDeleteCallback(
|
|||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
when (viewHolder) {
|
||||
is TabInCollectionViewHolder -> {
|
||||
interactor.onCollectionRemoveTab(viewHolder.collection, viewHolder.tab)
|
||||
interactor.onCollectionRemoveTab(viewHolder.collection, viewHolder.tab, wasSwiped = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ class TabInCollectionViewHolder(
|
|||
|
||||
list_item_action_button.increaseTapArea(buttonIncreaseDps)
|
||||
list_item_action_button.setOnClickListener {
|
||||
interactor.onCollectionRemoveTab(collection, tab)
|
||||
interactor.onCollectionRemoveTab(collection, tab, wasSwiped = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ import android.view.View
|
|||
import android.widget.PopupWindow
|
||||
import androidx.appcompat.content.res.AppCompatResources.getDrawable
|
||||
import kotlinx.android.synthetic.main.top_site_item.*
|
||||
import kotlinx.android.synthetic.main.top_site_item.view.*
|
||||
import mozilla.components.browser.menu.BrowserMenuBuilder
|
||||
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
|
||||
import mozilla.components.feature.top.sites.TopSite
|
||||
|
@ -44,7 +43,7 @@ class TopSiteItemViewHolder(
|
|||
}
|
||||
|
||||
top_site_item.setOnLongClickListener {
|
||||
val menu = topSiteMenu.menuBuilder.build(view.context).show(anchor = it.top_site_title)
|
||||
val menu = topSiteMenu.menuBuilder.build(view.context).show(anchor = it)
|
||||
it.setOnTouchListener @SuppressLint("ClickableViewAccessibility") { v, event ->
|
||||
onTouchEvent(v, event, menu)
|
||||
}
|
||||
|
|
|
@ -67,6 +67,8 @@ class LibrarySiteItemView @JvmOverloads constructor(
|
|||
|
||||
val overflowView: ImageButton get() = overflow_menu
|
||||
|
||||
private var iconUrl: String? = null
|
||||
|
||||
init {
|
||||
LayoutInflater.from(context).inflate(R.layout.library_site_item, this, true)
|
||||
|
||||
|
@ -94,6 +96,9 @@ class LibrarySiteItemView @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
fun loadFavicon(url: String) {
|
||||
if (iconUrl == url) return
|
||||
|
||||
iconUrl = url
|
||||
context.components.core.icons.loadIntoView(favicon, url)
|
||||
}
|
||||
|
||||
|
|
|
@ -68,10 +68,10 @@ class HistoryListItemViewHolder(
|
|||
itemView.history_layout.loadFavicon(item.url)
|
||||
}
|
||||
|
||||
if (item !in selectionHolder.selectedItems) {
|
||||
itemView.overflow_menu.showAndEnable()
|
||||
} else {
|
||||
if (mode is HistoryFragmentState.Mode.Editing) {
|
||||
itemView.overflow_menu.hideAndDisable()
|
||||
} else {
|
||||
itemView.overflow_menu.showAndEnable()
|
||||
}
|
||||
|
||||
this.item = item
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -170,7 +170,7 @@ class AwesomeBarView(
|
|||
updateSuggestionProvidersVisibility(state)
|
||||
|
||||
// Do not make suggestions based on user's current URL unless it's a search shortcut
|
||||
if (state.query == state.url && !state.showSearchShortcuts) {
|
||||
if (state.query.isNotEmpty() && state.query == state.url && !state.showSearchShortcuts) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import mozilla.components.concept.sync.AccountObserver
|
|||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.concept.sync.Profile
|
||||
import mozilla.components.support.ktx.android.content.hasCamera
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.Config
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
|
@ -188,7 +189,16 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
|
||||
val directions: NavDirections? = when (preference.key) {
|
||||
resources.getString(R.string.pref_key_sign_in) -> {
|
||||
SettingsFragmentDirections.actionSettingsFragmentToTurnOnSyncFragment()
|
||||
// App can be installed on devices with no camera modules. Like Android TV boxes.
|
||||
// Let's skip presenting the option to sign in by scanning a qr code in this case
|
||||
// and default to login with email and password.
|
||||
if (requireContext().hasCamera()) {
|
||||
SettingsFragmentDirections.actionSettingsFragmentToTurnOnSyncFragment()
|
||||
} else {
|
||||
requireComponents.services.accountsAuthFeature.beginAuthentication(requireContext())
|
||||
requireComponents.analytics.metrics.track(Event.SyncAuthUseEmail)
|
||||
null
|
||||
}
|
||||
}
|
||||
resources.getString(R.string.pref_key_search_settings) -> {
|
||||
SettingsFragmentDirections.actionSettingsFragmentToSearchEngineFragment()
|
||||
|
|
|
@ -17,6 +17,7 @@ import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
|||
import kotlinx.android.synthetic.main.fragment_about.*
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.BuildConfig
|
||||
import org.mozilla.fenix.Config
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
|
@ -38,6 +39,7 @@ import org.mozilla.geckoview.BuildConfig as GeckoViewBuildConfig
|
|||
*/
|
||||
class AboutFragment : Fragment(), AboutPageListener {
|
||||
|
||||
private lateinit var headerAppName: String
|
||||
private lateinit var appName: String
|
||||
private val aboutPageAdapter: AboutPageAdapter = AboutPageAdapter(this)
|
||||
|
||||
|
@ -48,6 +50,8 @@ class AboutFragment : Fragment(), AboutPageListener {
|
|||
): View? {
|
||||
val rootView = inflater.inflate(R.layout.fragment_about, container, false)
|
||||
appName = getString(R.string.app_name)
|
||||
headerAppName =
|
||||
if (Config.channel.isRelease) getString(R.string.daylight_app_name) else appName
|
||||
activity?.title = getString(R.string.preferences_about, appName)
|
||||
|
||||
return rootView
|
||||
|
@ -64,10 +68,12 @@ class AboutFragment : Fragment(), AboutPageListener {
|
|||
)
|
||||
}
|
||||
|
||||
lifecycle.addObserver(SecretDebugMenuTrigger(
|
||||
logoView = wordmark,
|
||||
settings = view.context.settings()
|
||||
))
|
||||
lifecycle.addObserver(
|
||||
SecretDebugMenuTrigger(
|
||||
logoView = wordmark,
|
||||
settings = view.context.settings()
|
||||
)
|
||||
)
|
||||
|
||||
populateAboutHeader()
|
||||
aboutPageAdapter.submitList(populateAboutList())
|
||||
|
@ -75,12 +81,15 @@ class AboutFragment : Fragment(), AboutPageListener {
|
|||
|
||||
private fun populateAboutHeader() {
|
||||
val aboutText = try {
|
||||
val packageInfo = requireContext().packageManager.getPackageInfo(requireContext().packageName, 0)
|
||||
val packageInfo =
|
||||
requireContext().packageManager.getPackageInfo(requireContext().packageName, 0)
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo).toString()
|
||||
val componentsAbbreviation = getString(R.string.components_abbreviation)
|
||||
val componentsVersion = mozilla.components.Build.version + ", " + mozilla.components.Build.gitHash
|
||||
val componentsVersion =
|
||||
mozilla.components.Build.version + ", " + mozilla.components.Build.gitHash
|
||||
val maybeGecko = getString(R.string.gecko_view_abbreviation)
|
||||
val geckoVersion = GeckoViewBuildConfig.MOZ_APP_VERSION + "-" + GeckoViewBuildConfig.MOZ_APP_BUILDID
|
||||
val geckoVersion =
|
||||
GeckoViewBuildConfig.MOZ_APP_VERSION + "-" + GeckoViewBuildConfig.MOZ_APP_BUILDID
|
||||
val appServicesAbbreviation = getString(R.string.app_services_abbreviation)
|
||||
val appServicesVersion = mozilla.components.Build.applicationServicesVersion
|
||||
|
||||
|
@ -99,7 +108,7 @@ class AboutFragment : Fragment(), AboutPageListener {
|
|||
""
|
||||
}
|
||||
|
||||
val content = getString(R.string.about_content, appName)
|
||||
val content = getString(R.string.about_content, headerAppName)
|
||||
val buildDate = BuildConfig.BUILD_DATE
|
||||
|
||||
about_text.text = aboutText
|
||||
|
@ -160,7 +169,12 @@ class AboutFragment : Fragment(), AboutPageListener {
|
|||
|
||||
private fun openLibrariesPage() {
|
||||
startActivity(Intent(context, OssLicensesMenuActivity::class.java))
|
||||
OssLicensesMenuActivity.setActivityTitle(getString(R.string.open_source_licenses_title, appName))
|
||||
OssLicensesMenuActivity.setActivityTitle(
|
||||
getString(
|
||||
R.string.open_source_licenses_title,
|
||||
appName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onAboutItemClicked(item: AboutItem) {
|
||||
|
|
|
@ -13,6 +13,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.FenixSnackbar
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.settings
|
||||
|
||||
/**
|
||||
|
@ -21,7 +22,13 @@ import org.mozilla.fenix.ext.settings
|
|||
fun deleteAndQuit(activity: Activity, coroutineScope: CoroutineScope, snackbar: FenixSnackbar?) {
|
||||
coroutineScope.launch {
|
||||
val settings = activity.settings()
|
||||
val controller = DefaultDeleteBrowsingDataController(activity, coroutineContext)
|
||||
val controller = DefaultDeleteBrowsingDataController(
|
||||
activity.components.useCases.tabsUseCases.removeAllTabs,
|
||||
activity.components.core.historyStorage,
|
||||
activity.components.core.permissionStorage,
|
||||
activity.components.core.engine,
|
||||
coroutineContext
|
||||
)
|
||||
|
||||
snackbar?.apply {
|
||||
setText(activity.getString(R.string.deleting_browsing_data_in_progress))
|
||||
|
|
|
@ -4,11 +4,12 @@
|
|||
|
||||
package org.mozilla.fenix.settings.deletebrowsingdata
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import org.mozilla.fenix.ext.components
|
||||
import mozilla.components.concept.storage.HistoryStorage
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import org.mozilla.fenix.components.PermissionStorage
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
interface DeleteBrowsingDataController {
|
||||
|
@ -21,13 +22,16 @@ interface DeleteBrowsingDataController {
|
|||
}
|
||||
|
||||
class DefaultDeleteBrowsingDataController(
|
||||
val context: Context,
|
||||
val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
private val removeAllTabs: TabsUseCases.RemoveAllTabsUseCase,
|
||||
private val historyStorage: HistoryStorage,
|
||||
private val permissionStorage: PermissionStorage,
|
||||
private val engine: Engine,
|
||||
private val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
) : DeleteBrowsingDataController {
|
||||
|
||||
override suspend fun deleteTabs() {
|
||||
withContext(coroutineContext) {
|
||||
context.components.useCases.tabsUseCases.removeAllTabs.invoke()
|
||||
removeAllTabs.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,14 +41,14 @@ class DefaultDeleteBrowsingDataController(
|
|||
|
||||
override suspend fun deleteHistoryAndDOMStorages() {
|
||||
withContext(coroutineContext) {
|
||||
context.components.core.engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES))
|
||||
engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES))
|
||||
}
|
||||
context.components.core.historyStorage.deleteEverything()
|
||||
historyStorage.deleteEverything()
|
||||
}
|
||||
|
||||
override suspend fun deleteCookies() {
|
||||
withContext(coroutineContext) {
|
||||
context.components.core.engine.clearData(
|
||||
engine.clearData(
|
||||
Engine.BrowsingData.select(
|
||||
Engine.BrowsingData.COOKIES,
|
||||
Engine.BrowsingData.AUTH_SESSIONS
|
||||
|
@ -55,7 +59,7 @@ class DefaultDeleteBrowsingDataController(
|
|||
|
||||
override suspend fun deleteCachedFiles() {
|
||||
withContext(coroutineContext) {
|
||||
context.components.core.engine.clearData(
|
||||
engine.clearData(
|
||||
Engine.BrowsingData.select(Engine.BrowsingData.ALL_CACHES)
|
||||
)
|
||||
}
|
||||
|
@ -63,10 +67,10 @@ class DefaultDeleteBrowsingDataController(
|
|||
|
||||
override suspend fun deleteSitePermissions() {
|
||||
withContext(coroutineContext) {
|
||||
context.components.core.engine.clearData(
|
||||
engine.clearData(
|
||||
Engine.BrowsingData.select(Engine.BrowsingData.ALL_SITE_SETTINGS)
|
||||
)
|
||||
}
|
||||
context.components.core.permissionStorage.deleteAllSitePermissions()
|
||||
permissionStorage.deleteAllSitePermissions()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
|
|||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.FenixSnackbar
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
|
@ -40,7 +41,12 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
controller = DefaultDeleteBrowsingDataController(requireContext())
|
||||
controller = DefaultDeleteBrowsingDataController(
|
||||
requireContext().components.useCases.tabsUseCases.removeAllTabs,
|
||||
requireContext().components.core.historyStorage,
|
||||
requireContext().components.core.permissionStorage,
|
||||
requireContext().components.core.engine
|
||||
)
|
||||
settings = requireContext().settings()
|
||||
|
||||
getCheckboxes().forEach {
|
||||
|
|
|
@ -5,51 +5,68 @@
|
|||
package org.mozilla.fenix.settings.logins
|
||||
|
||||
import android.content.Context
|
||||
import mozilla.components.browser.menu.BrowserMenuBuilder
|
||||
import mozilla.components.browser.menu.item.SimpleBrowserMenuHighlightableItem
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import mozilla.components.browser.menu2.BrowserMenuController
|
||||
import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
|
||||
import mozilla.components.concept.menu.candidate.TextMenuCandidate
|
||||
import mozilla.components.concept.menu.candidate.TextStyle
|
||||
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.theme.ThemeManager
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
|
||||
|
||||
class SavedLoginsSortingStrategyMenu(
|
||||
private val context: Context,
|
||||
private val itemToHighlight: Item,
|
||||
private val onItemTapped: (Item) -> Unit = {}
|
||||
private val savedLoginsInteractor: SavedLoginsInteractor
|
||||
) {
|
||||
sealed class Item {
|
||||
object AlphabeticallySort : Item()
|
||||
object LastUsedSort : Item()
|
||||
enum class Item(val strategyString: String) {
|
||||
AlphabeticallySort("ALPHABETICALLY"),
|
||||
LastUsedSort("LAST_USED");
|
||||
|
||||
companion object {
|
||||
fun fromString(strategyString: String) = when (strategyString) {
|
||||
AlphabeticallySort.strategyString -> AlphabeticallySort
|
||||
LastUsedSort.strategyString -> LastUsedSort
|
||||
else -> AlphabeticallySort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
|
||||
val menuController by lazy { BrowserMenuController() }
|
||||
|
||||
private val menuItems by lazy {
|
||||
listOfNotNull(
|
||||
SimpleBrowserMenuHighlightableItem(
|
||||
label = context.getString(R.string.saved_logins_sort_strategy_alphabetically),
|
||||
textColorResource = ThemeManager.resolveAttribute(R.attr.primaryText, context),
|
||||
itemType = Item.AlphabeticallySort,
|
||||
backgroundTint = context.getColorFromAttr(R.attr.colorControlHighlight),
|
||||
isHighlighted = { itemToHighlight == Item.AlphabeticallySort }
|
||||
@VisibleForTesting
|
||||
internal fun menuItems(itemToHighlight: Item): List<TextMenuCandidate> {
|
||||
val textStyle = TextStyle(
|
||||
color = context.getColorFromAttr(R.attr.primaryText)
|
||||
)
|
||||
|
||||
val highlight = HighPriorityHighlightEffect(
|
||||
backgroundTint = context.getColorFromAttr(R.attr.colorControlHighlight)
|
||||
)
|
||||
|
||||
return listOf(
|
||||
TextMenuCandidate(
|
||||
text = context.getString(R.string.saved_logins_sort_strategy_alphabetically),
|
||||
textStyle = textStyle,
|
||||
effect = if (itemToHighlight == Item.AlphabeticallySort) highlight else null
|
||||
) {
|
||||
onItemTapped.invoke(Item.AlphabeticallySort)
|
||||
savedLoginsInteractor.onSortingStrategyChanged(
|
||||
SortingStrategy.Alphabetically(context.components.publicSuffixList)
|
||||
)
|
||||
},
|
||||
|
||||
SimpleBrowserMenuHighlightableItem(
|
||||
label = context.getString(R.string.saved_logins_sort_strategy_last_used),
|
||||
textColorResource = ThemeManager.resolveAttribute(R.attr.primaryText, context),
|
||||
itemType = Item.LastUsedSort,
|
||||
backgroundTint = context.getColorFromAttr(R.attr.colorControlHighlight),
|
||||
isHighlighted = { itemToHighlight == Item.LastUsedSort }
|
||||
TextMenuCandidate(
|
||||
text = context.getString(R.string.saved_logins_sort_strategy_last_used),
|
||||
textStyle = textStyle,
|
||||
effect = if (itemToHighlight == Item.LastUsedSort) highlight else null
|
||||
) {
|
||||
onItemTapped.invoke(Item.LastUsedSort)
|
||||
savedLoginsInteractor.onSortingStrategyChanged(
|
||||
SortingStrategy.LastUsed
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
internal fun updateMenu(itemToHighlight: Item) {
|
||||
menuItems.forEach {
|
||||
it.isHighlighted = { itemToHighlight == it.itemType }
|
||||
}
|
||||
fun updateMenu(itemToHighlight: Item) {
|
||||
menuController.submitList(menuItems(itemToHighlight))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,16 +4,21 @@
|
|||
|
||||
package org.mozilla.fenix.settings.logins
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.navigation.NavController
|
||||
import androidx.preference.Preference
|
||||
import mozilla.components.concept.sync.AccountObserver
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature
|
||||
import mozilla.components.service.fxa.SyncEngine
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
import mozilla.components.service.fxa.manager.SyncEnginesStorage
|
||||
import mozilla.components.support.ktx.android.content.hasCamera
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections
|
||||
|
||||
/**
|
||||
|
@ -23,7 +28,9 @@ class SyncLoginsPreferenceView(
|
|||
private val syncLoginsPreference: Preference,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
accountManager: FxaAccountManager,
|
||||
private val navController: NavController
|
||||
private val navController: NavController,
|
||||
private val accountsAuthFeature: FirefoxAccountsAuthFeature,
|
||||
private val metrics: MetricController
|
||||
) {
|
||||
|
||||
init {
|
||||
|
@ -68,7 +75,15 @@ class SyncLoginsPreferenceView(
|
|||
syncLoginsPreference.apply {
|
||||
summary = context.getString(R.string.preferences_passwords_sync_logins_sign_in)
|
||||
setOnPreferenceClickListener {
|
||||
navigateToTurnOnSyncFragment()
|
||||
// App can be installed on devices with no camera modules. Like Android TV boxes.
|
||||
// Let's skip presenting the option to sign in by scanning a qr code in this case
|
||||
// and default to login with email and password.
|
||||
if (context.hasCamera()) {
|
||||
navigateToTurnOnSyncFragment()
|
||||
} else {
|
||||
navigateToPairWithEmail(context)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -102,4 +117,9 @@ class SyncLoginsPreferenceView(
|
|||
val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToTurnOnSyncFragment()
|
||||
navController.navigate(directions)
|
||||
}
|
||||
|
||||
private fun navigateToPairWithEmail(context: Context) {
|
||||
accountsAuthFeature.beginAuthentication(context)
|
||||
metrics.track(Event.SyncAuthUseEmail)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
package org.mozilla.fenix.settings.logins.controller
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.navigation.NavController
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -18,8 +18,8 @@ import mozilla.components.concept.storage.Login
|
|||
import mozilla.components.service.sync.logins.InvalidRecordException
|
||||
import mozilla.components.service.sync.logins.LoginsStorageException
|
||||
import mozilla.components.service.sync.logins.NoSuchRecordException
|
||||
import mozilla.components.service.sync.logins.SyncableLoginsStorage
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.settings.logins.LoginsAction
|
||||
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
|
||||
import org.mozilla.fenix.settings.logins.fragment.EditLoginFragmentDirections
|
||||
|
@ -29,20 +29,20 @@ import org.mozilla.fenix.settings.logins.mapToSavedLogin
|
|||
* Controller for all saved logins interactions with the password storage component
|
||||
*/
|
||||
open class SavedLoginsStorageController(
|
||||
private val context: Context,
|
||||
private val passwordsStorage: SyncableLoginsStorage,
|
||||
private val viewLifecycleScope: CoroutineScope,
|
||||
private val navController: NavController,
|
||||
private val loginsFragmentStore: LoginsFragmentStore
|
||||
private val loginsFragmentStore: LoginsFragmentStore,
|
||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) {
|
||||
|
||||
private suspend fun getLogin(loginId: String): Login? =
|
||||
context.components.core.passwordsStorage.get(loginId)
|
||||
private suspend fun getLogin(loginId: String): Login? = passwordsStorage.get(loginId)
|
||||
|
||||
fun delete(loginId: String) {
|
||||
var deleteLoginJob: Deferred<Boolean>? = null
|
||||
val deleteJob = viewLifecycleScope.launch(Dispatchers.IO) {
|
||||
val deleteJob = viewLifecycleScope.launch(ioDispatcher) {
|
||||
deleteLoginJob = async {
|
||||
context.components.core.passwordsStorage.delete(loginId)
|
||||
passwordsStorage.delete(loginId)
|
||||
}
|
||||
deleteLoginJob?.await()
|
||||
withContext(Dispatchers.Main) {
|
||||
|
@ -58,10 +58,10 @@ open class SavedLoginsStorageController(
|
|||
|
||||
fun save(loginId: String, usernameText: String, passwordText: String) {
|
||||
var saveLoginJob: Deferred<Unit>? = null
|
||||
viewLifecycleScope.launch(Dispatchers.IO) {
|
||||
viewLifecycleScope.launch(ioDispatcher) {
|
||||
saveLoginJob = async {
|
||||
// must retrieve from storage to get the httpsRealm and formActionOrigin
|
||||
val oldLogin = context.components.core.passwordsStorage.get(loginId)
|
||||
val oldLogin = passwordsStorage.get(loginId)
|
||||
|
||||
// Update requires a Login type, which needs at least one of
|
||||
// httpRealm or formActionOrigin
|
||||
|
@ -95,16 +95,20 @@ open class SavedLoginsStorageController(
|
|||
|
||||
private suspend fun save(loginToSave: Login) {
|
||||
try {
|
||||
context.components.core.passwordsStorage.update(loginToSave)
|
||||
passwordsStorage.update(loginToSave)
|
||||
} catch (loginException: LoginsStorageException) {
|
||||
when (loginException) {
|
||||
is NoSuchRecordException,
|
||||
is InvalidRecordException -> {
|
||||
Log.e("Edit login",
|
||||
"Failed to save edited login.", loginException)
|
||||
Log.e(
|
||||
"Edit login",
|
||||
"Failed to save edited login.", loginException
|
||||
)
|
||||
}
|
||||
else -> Log.e("Edit login",
|
||||
"Failed to save edited login.", loginException)
|
||||
else -> Log.e(
|
||||
"Edit login",
|
||||
"Failed to save edited login.", loginException
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,10 +125,10 @@ open class SavedLoginsStorageController(
|
|||
fun findPotentialDuplicates(loginId: String) {
|
||||
var deferredLogin: Deferred<List<Login>>? = null
|
||||
// What scope should be used here?
|
||||
val fetchLoginJob = viewLifecycleScope.launch(Dispatchers.IO) {
|
||||
val fetchLoginJob = viewLifecycleScope.launch(ioDispatcher) {
|
||||
deferredLogin = async {
|
||||
val login = getLogin(loginId)
|
||||
context.components.core.passwordsStorage.getPotentialDupesIgnoringUsername(login!!)
|
||||
passwordsStorage.getPotentialDupesIgnoringUsername(login!!)
|
||||
}
|
||||
val fetchedDuplicatesList = deferredLogin?.await()
|
||||
fetchedDuplicatesList?.let { list ->
|
||||
|
@ -147,9 +151,9 @@ open class SavedLoginsStorageController(
|
|||
|
||||
fun fetchLoginDetails(loginId: String) {
|
||||
var deferredLogin: Deferred<List<Login>>? = null
|
||||
val fetchLoginJob = viewLifecycleScope.launch(Dispatchers.IO) {
|
||||
val fetchLoginJob = viewLifecycleScope.launch(ioDispatcher) {
|
||||
deferredLogin = async {
|
||||
context.components.core.passwordsStorage.list()
|
||||
passwordsStorage.list()
|
||||
}
|
||||
val fetchedLoginList = deferredLogin?.await()
|
||||
|
||||
|
@ -175,9 +179,9 @@ open class SavedLoginsStorageController(
|
|||
|
||||
fun handleLoadAndMapLogins() {
|
||||
var deferredLogins: Deferred<List<Login>>? = null
|
||||
val fetchLoginsJob = viewLifecycleScope.launch(Dispatchers.IO) {
|
||||
val fetchLoginsJob = viewLifecycleScope.launch(ioDispatcher) {
|
||||
deferredLogins = async {
|
||||
context.components.core.passwordsStorage.list()
|
||||
passwordsStorage.list()
|
||||
}
|
||||
val logins = deferredLogins?.await()
|
||||
logins?.let {
|
||||
|
|
|
@ -24,6 +24,7 @@ import mozilla.components.support.ktx.android.view.hideKeyboard
|
|||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.redirectToReAuth
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.settings
|
||||
|
@ -79,7 +80,7 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
|
|||
|
||||
interactor = EditLoginInteractor(
|
||||
SavedLoginsStorageController(
|
||||
context = requireContext(),
|
||||
passwordsStorage = requireContext().components.core.passwordsStorage,
|
||||
viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
|
||||
navController = findNavController(),
|
||||
loginsFragmentStore = loginsFragmentStore
|
||||
|
|
|
@ -94,7 +94,7 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) {
|
|||
|
||||
interactor = LoginDetailInteractor(
|
||||
SavedLoginsStorageController(
|
||||
context = requireContext(),
|
||||
passwordsStorage = requireContext().components.core.passwordsStorage,
|
||||
viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
|
||||
navController = findNavController(),
|
||||
loginsFragmentStore = savedLoginsStore
|
||||
|
|
|
@ -145,7 +145,9 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() {
|
|||
requirePreference(R.string.pref_key_password_sync_logins),
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
accountManager = requireComponents.backgroundServices.accountManager,
|
||||
navController = findNavController()
|
||||
navController = findNavController(),
|
||||
accountsAuthFeature = requireComponents.services.accountsAuthFeature,
|
||||
metrics = requireComponents.analytics.metrics
|
||||
)
|
||||
|
||||
togglePrefsEnabledWhileAuthenticating(enabled = true)
|
||||
|
|
|
@ -22,8 +22,8 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.android.synthetic.main.fragment_saved_logins.view.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.ObsoleteCoroutinesApi
|
||||
import mozilla.components.browser.menu.BrowserMenu
|
||||
import mozilla.components.concept.menu.MenuController
|
||||
import mozilla.components.concept.menu.Orientation
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
|
@ -31,7 +31,6 @@ import org.mozilla.fenix.R
|
|||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.redirectToReAuth
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.settings.logins.LoginsAction
|
||||
|
@ -51,7 +50,6 @@ class SavedLoginsFragment : Fragment() {
|
|||
private lateinit var savedLoginsInteractor: SavedLoginsInteractor
|
||||
private lateinit var dropDownMenuAnchorView: View
|
||||
private lateinit var sortingStrategyMenu: SavedLoginsSortingStrategyMenu
|
||||
private lateinit var sortingStrategyPopupMenu: BrowserMenu
|
||||
private lateinit var toolbarChildContainer: FrameLayout
|
||||
private lateinit var sortLoginsMenuRoot: ConstraintLayout
|
||||
private lateinit var loginsListController: LoginsListController
|
||||
|
@ -101,7 +99,7 @@ class SavedLoginsFragment : Fragment() {
|
|||
)
|
||||
savedLoginsStorageController =
|
||||
SavedLoginsStorageController(
|
||||
context = requireContext(),
|
||||
passwordsStorage = requireContext().components.core.passwordsStorage,
|
||||
viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
|
||||
navController = findNavController(),
|
||||
loginsFragmentStore = savedLoginsStore
|
||||
|
@ -121,10 +119,8 @@ class SavedLoginsFragment : Fragment() {
|
|||
return view
|
||||
}
|
||||
|
||||
@ObsoleteCoroutinesApi
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
consumeFrom(savedLoginsStore) {
|
||||
sortingStrategyMenu.updateMenu(savedLoginsStore.state.highlightedItem)
|
||||
savedLoginsListView.update(it)
|
||||
|
@ -161,7 +157,7 @@ class SavedLoginsFragment : Fragment() {
|
|||
toolbarChildContainer.removeAllViews()
|
||||
toolbarChildContainer.visibility = View.GONE
|
||||
(activity as HomeActivity).getSupportActionBarAndInflateIfNecessary().setDisplayShowTitleEnabled(true)
|
||||
sortingStrategyPopupMenu.dismiss()
|
||||
sortingStrategyMenu.menuController.dismiss()
|
||||
|
||||
redirectToReAuth(listOf(R.id.loginDetailFragment), findNavController().currentDestination?.id)
|
||||
super.onPause()
|
||||
|
@ -206,47 +202,27 @@ class SavedLoginsFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun attachMenu() {
|
||||
sortingStrategyPopupMenu = sortingStrategyMenu.menuBuilder.build(requireContext())
|
||||
|
||||
sortLoginsMenuRoot.setOnClickListener {
|
||||
sortLoginsMenuRoot.isActivated = true
|
||||
sortingStrategyPopupMenu.show(
|
||||
anchor = dropDownMenuAnchorView,
|
||||
orientation = BrowserMenu.Orientation.DOWN
|
||||
) {
|
||||
sortingStrategyMenu.menuController.register(object : MenuController.Observer {
|
||||
override fun onDismiss() {
|
||||
// Deactivate button on dismiss
|
||||
sortLoginsMenuRoot.isActivated = false
|
||||
}
|
||||
}, view = sortLoginsMenuRoot)
|
||||
|
||||
sortLoginsMenuRoot.setOnClickListener {
|
||||
// Activate button on show
|
||||
sortLoginsMenuRoot.isActivated = true
|
||||
sortingStrategyMenu.menuController.show(
|
||||
anchor = dropDownMenuAnchorView,
|
||||
orientation = Orientation.DOWN
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMenu(itemToHighlight: SavedLoginsSortingStrategyMenu.Item) {
|
||||
sortingStrategyMenu =
|
||||
SavedLoginsSortingStrategyMenu(
|
||||
requireContext(),
|
||||
itemToHighlight
|
||||
) {
|
||||
when (it) {
|
||||
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort -> {
|
||||
savedLoginsInteractor.onSortingStrategyChanged(
|
||||
SortingStrategy.Alphabetically(
|
||||
requireComponents.publicSuffixList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
SavedLoginsSortingStrategyMenu.Item.LastUsedSort -> {
|
||||
savedLoginsInteractor.onSortingStrategyChanged(
|
||||
SortingStrategy.LastUsed
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
sortingStrategyMenu = SavedLoginsSortingStrategyMenu(requireContext(), savedLoginsInteractor)
|
||||
sortingStrategyMenu.updateMenu(itemToHighlight)
|
||||
|
||||
attachMenu()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SORTING_STRATEGY_ALPHABETICALLY = "ALPHABETICALLY"
|
||||
const val SORTING_STRATEGY_LAST_USED = "LAST_USED"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,21 +5,18 @@
|
|||
package org.mozilla.fenix.settings.logins.view
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.logins_item.view.*
|
||||
import kotlinx.android.synthetic.main.logins_item.*
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.loadIntoView
|
||||
import org.mozilla.fenix.settings.logins.SavedLogin
|
||||
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
|
||||
import org.mozilla.fenix.utils.view.ViewHolder
|
||||
|
||||
class LoginsListViewHolder(
|
||||
private val view: View,
|
||||
view: View,
|
||||
private val interactor: SavedLoginsInteractor
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
) : ViewHolder(view) {
|
||||
|
||||
private val favicon = view.favicon_image
|
||||
private val url = view.webAddressView
|
||||
private val username = view.usernameView
|
||||
private var loginItem: SavedLogin? = null
|
||||
|
||||
fun bind(item: SavedLogin) {
|
||||
|
@ -30,17 +27,17 @@ class LoginsListViewHolder(
|
|||
username = item.username,
|
||||
timeLastUsed = item.timeLastUsed
|
||||
)
|
||||
url.text = item.origin
|
||||
username.text = item.username
|
||||
webAddressView.text = item.origin
|
||||
usernameView.text = item.username
|
||||
|
||||
updateFavIcon(item.origin)
|
||||
|
||||
view.setOnClickListener {
|
||||
itemView.setOnClickListener {
|
||||
interactor.onItemClicked(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFavIcon(url: String) {
|
||||
favicon.context.components.core.icons.loadIntoView(favicon, url)
|
||||
itemView.context.components.core.icons.loadIntoView(favicon_image, url)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@ import androidx.preference.Preference
|
|||
import androidx.preference.Preference.OnPreferenceClickListener
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.getPreferenceKey
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.settings.PhoneFeature
|
||||
|
@ -78,6 +80,11 @@ class SitePermissionsFragment : PreferenceFragmentCompat() {
|
|||
private fun navigateToPhoneFeature(phoneFeature: PhoneFeature) {
|
||||
val directions = SitePermissionsFragmentDirections
|
||||
.actionSitePermissionsToManagePhoneFeatures(phoneFeature)
|
||||
|
||||
if (phoneFeature == PhoneFeature.AUTOPLAY_AUDIBLE) {
|
||||
requireComponents.analytics.metrics.track(Event.AutoPlaySettingVisited)
|
||||
}
|
||||
|
||||
Navigation.findNavController(requireView()).navigate(directions)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.AL
|
|||
import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.ASK_TO_ALLOW
|
||||
import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.BLOCKED
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_AUDIBLE
|
||||
|
@ -180,16 +182,27 @@ class SitePermissionsManagePhoneFeatureFragment : Fragment() {
|
|||
*/
|
||||
private fun saveActionInSettings(autoplaySetting: Int) {
|
||||
settings.setAutoplayUserSetting(autoplaySetting)
|
||||
val setting: Event.AutoPlaySettingChanged.AutoplaySetting
|
||||
|
||||
val (audible, inaudible) = when (autoplaySetting) {
|
||||
AUTOPLAY_ALLOW_ALL,
|
||||
AUTOPLAY_ALLOW_ON_WIFI -> {
|
||||
settings.setAutoplayUserSetting(AUTOPLAY_ALLOW_ON_WIFI)
|
||||
setting = Event.AutoPlaySettingChanged.AutoplaySetting.BLOCK_CELLULAR
|
||||
BLOCKED to BLOCKED
|
||||
}
|
||||
AUTOPLAY_BLOCK_AUDIBLE -> {
|
||||
setting = Event.AutoPlaySettingChanged.AutoplaySetting.BLOCK_AUDIO
|
||||
BLOCKED to ALLOWED
|
||||
}
|
||||
AUTOPLAY_BLOCK_ALL -> {
|
||||
setting = Event.AutoPlaySettingChanged.AutoplaySetting.BLOCK_ALL
|
||||
BLOCKED to BLOCKED
|
||||
}
|
||||
AUTOPLAY_BLOCK_AUDIBLE -> BLOCKED to ALLOWED
|
||||
AUTOPLAY_BLOCK_ALL -> BLOCKED to BLOCKED
|
||||
else -> return
|
||||
}
|
||||
|
||||
requireComponents.analytics.metrics.track(Event.AutoPlaySettingChanged(setting))
|
||||
settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, audible)
|
||||
settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, inaudible)
|
||||
}
|
||||
|
|
|
@ -38,7 +38,9 @@ class PwaOnboardingDialogFragment : DialogFragment() {
|
|||
add_button.setOnClickListener {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
components.useCases.webAppUseCases.addToHomescreen()
|
||||
}.invokeOnCompletion { dismiss() }
|
||||
}.invokeOnCompletion {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,12 @@ package org.mozilla.fenix.sync
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.navigation.NavController
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
|
||||
import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder
|
||||
import org.mozilla.fenix.sync.SyncedTabsViewHolder.ErrorViewHolder
|
||||
import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder
|
||||
import mozilla.components.browser.storage.sync.Tab as SyncTab
|
||||
import mozilla.components.concept.sync.Device as SyncDevice
|
||||
|
@ -24,6 +26,7 @@ class SyncedTabsAdapter(
|
|||
return when (viewType) {
|
||||
DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView)
|
||||
TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView)
|
||||
ErrorViewHolder.LAYOUT_ID -> ErrorViewHolder(itemView)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
@ -33,8 +36,9 @@ class SyncedTabsAdapter(
|
|||
}
|
||||
|
||||
override fun getItemViewType(position: Int) = when (getItem(position)) {
|
||||
is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID
|
||||
is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID
|
||||
is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID
|
||||
is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID
|
||||
is AdapterItem.Error -> ErrorViewHolder.LAYOUT_ID
|
||||
}
|
||||
|
||||
fun updateData(syncedTabs: List<SyncedDeviceTabs>) {
|
||||
|
@ -55,7 +59,7 @@ class SyncedTabsAdapter(
|
|||
when (oldItem) {
|
||||
is AdapterItem.Device ->
|
||||
newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id
|
||||
is AdapterItem.Tab ->
|
||||
is AdapterItem.Tab, is AdapterItem.Error ->
|
||||
oldItem == newItem
|
||||
}
|
||||
|
||||
|
@ -67,5 +71,9 @@ class SyncedTabsAdapter(
|
|||
sealed class AdapterItem {
|
||||
data class Device(val device: SyncDevice) : AdapterItem()
|
||||
data class Tab(val tab: SyncTab) : AdapterItem()
|
||||
data class Error(
|
||||
val descriptionResId: Int,
|
||||
val navController: NavController? = null
|
||||
) : AdapterItem()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,11 @@ package org.mozilla.fenix.sync
|
|||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.findFragment
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.android.synthetic.main.component_sync_tabs.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -17,6 +20,7 @@ import kotlinx.coroutines.launch
|
|||
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
|
||||
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
|
||||
import org.mozilla.fenix.R
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
class SyncedTabsLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
|
@ -43,10 +47,17 @@ class SyncedTabsLayout @JvmOverloads constructor(
|
|||
// We may still be displaying a "loading" spinner, hide it.
|
||||
stopLoading()
|
||||
|
||||
sync_tabs_status.text = context.getText(stringResourceForError(error))
|
||||
val navController: NavController? = try {
|
||||
findFragment<SyncedTabsFragment>().findNavController()
|
||||
} catch (exception: IllegalStateException) {
|
||||
null
|
||||
}
|
||||
|
||||
synced_tabs_list.visibility = View.GONE
|
||||
sync_tabs_status.visibility = View.VISIBLE
|
||||
val descriptionResId = stringResourceForError(error)
|
||||
val errorItem = getErrorItem(navController, error, descriptionResId)
|
||||
|
||||
val errorList: List<SyncedTabsAdapter.AdapterItem> = listOf(errorItem)
|
||||
adapter.submitList(errorList)
|
||||
|
||||
synced_tabs_pull_to_refresh.isEnabled = pullToRefreshEnableState(error)
|
||||
}
|
||||
|
@ -54,17 +65,11 @@ class SyncedTabsLayout @JvmOverloads constructor(
|
|||
|
||||
override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) {
|
||||
coroutineScope.launch {
|
||||
synced_tabs_list.visibility = View.VISIBLE
|
||||
sync_tabs_status.visibility = View.GONE
|
||||
|
||||
adapter.updateData(syncedTabs)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startLoading() {
|
||||
synced_tabs_list.visibility = View.VISIBLE
|
||||
sync_tabs_status.visibility = View.GONE
|
||||
|
||||
synced_tabs_pull_to_refresh.isRefreshing = true
|
||||
}
|
||||
|
||||
|
@ -78,6 +83,7 @@ class SyncedTabsLayout @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
internal fun pullToRefreshEnableState(error: SyncedTabsView.ErrorType) = when (error) {
|
||||
// Disable "pull-to-refresh" when we clearly can't sync tabs, and user needs to take an
|
||||
// action within the app.
|
||||
|
@ -94,9 +100,23 @@ class SyncedTabsLayout @JvmOverloads constructor(
|
|||
internal fun stringResourceForError(error: SyncedTabsView.ErrorType) = when (error) {
|
||||
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device
|
||||
SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing
|
||||
SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_connect_to_sync_account
|
||||
SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_sign_in_message
|
||||
SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth
|
||||
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs
|
||||
}
|
||||
|
||||
internal fun getErrorItem(
|
||||
navController: NavController?,
|
||||
error: SyncedTabsView.ErrorType,
|
||||
@StringRes stringResId: Int
|
||||
): SyncedTabsAdapter.AdapterItem = when (error) {
|
||||
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
|
||||
SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE,
|
||||
SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION,
|
||||
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> SyncedTabsAdapter.AdapterItem
|
||||
.Error(descriptionResId = stringResId)
|
||||
SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> SyncedTabsAdapter.AdapterItem
|
||||
.Error(descriptionResId = stringResId, navController = navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,17 @@
|
|||
package org.mozilla.fenix.sync
|
||||
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.sync_tabs_error_row.view.*
|
||||
import kotlinx.android.synthetic.main.sync_tabs_list_item.view.*
|
||||
import kotlinx.android.synthetic.main.view_synced_tabs_group.view.*
|
||||
import mozilla.components.browser.storage.sync.Tab
|
||||
import mozilla.components.concept.sync.DeviceType
|
||||
import mozilla.components.support.ktx.android.util.dpToPx
|
||||
import org.mozilla.fenix.NavGraphDirections
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem
|
||||
|
||||
|
@ -38,6 +44,29 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
|
|||
}
|
||||
}
|
||||
|
||||
class ErrorViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
|
||||
|
||||
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) {
|
||||
val errorItem = item as AdapterItem.Error
|
||||
setErrorMargins()
|
||||
|
||||
itemView.sync_tabs_error_description.text =
|
||||
itemView.context.getString(errorItem.descriptionResId)
|
||||
itemView.sync_tabs_error_cta_button.visibility = GONE
|
||||
|
||||
errorItem.navController?.let { navController ->
|
||||
itemView.sync_tabs_error_cta_button.visibility = VISIBLE
|
||||
itemView.sync_tabs_error_cta_button.setOnClickListener {
|
||||
navController.navigate(NavGraphDirections.actionGlobalTurnOnSync())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LAYOUT_ID = R.layout.sync_tabs_error_row
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
|
||||
|
||||
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) {
|
||||
|
@ -45,18 +74,37 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
|
|||
}
|
||||
|
||||
private fun bindHeader(device: AdapterItem.Device) {
|
||||
|
||||
val deviceLogoDrawable = when (device.device.deviceType) {
|
||||
DeviceType.DESKTOP -> R.drawable.mozac_ic_device_desktop
|
||||
else -> R.drawable.mozac_ic_device_mobile
|
||||
}
|
||||
|
||||
itemView.synced_tabs_group_name.text = device.device.displayName
|
||||
itemView.synced_tabs_group_name.setCompoundDrawablesWithIntrinsicBounds(deviceLogoDrawable, 0, 0, 0)
|
||||
itemView.synced_tabs_group_name.setCompoundDrawablesWithIntrinsicBounds(
|
||||
deviceLogoDrawable,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LAYOUT_ID = R.layout.view_synced_tabs_group
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setErrorMargins() {
|
||||
val lp = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
val displayMetrics = itemView.context.resources.displayMetrics
|
||||
val margin = ERROR_MARGIN.dpToPx(displayMetrics)
|
||||
lp.setMargins(margin, margin, margin, 0)
|
||||
itemView.layoutParams = lp
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ERROR_MARGIN = 20
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@ package org.mozilla.fenix.tabhistory
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
data class TabHistoryItem(
|
||||
|
@ -18,23 +19,23 @@ data class TabHistoryItem(
|
|||
|
||||
class TabHistoryAdapter(
|
||||
private val interactor: TabHistoryViewInteractor
|
||||
) : RecyclerView.Adapter<TabHistoryViewHolder>() {
|
||||
|
||||
var historyList: List<TabHistoryItem> = emptyList()
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
) : ListAdapter<TabHistoryItem, TabHistoryViewHolder>(DiffCallback) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabHistoryViewHolder {
|
||||
val view =
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.history_list_item, parent, false)
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.tab_history_list_item, parent, false)
|
||||
return TabHistoryViewHolder(view, interactor)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TabHistoryViewHolder, position: Int) {
|
||||
holder.bind(historyList[position])
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = historyList.size
|
||||
internal object DiffCallback : DiffUtil.ItemCallback<TabHistoryItem>() {
|
||||
override fun areItemsTheSame(oldItem: TabHistoryItem, newItem: TabHistoryItem) =
|
||||
oldItem.url == newItem.url
|
||||
|
||||
override fun areContentsTheSame(oldItem: TabHistoryItem, newItem: TabHistoryItem) =
|
||||
oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,19 +24,16 @@ interface TabHistoryViewInteractor {
|
|||
}
|
||||
|
||||
class TabHistoryView(
|
||||
private val container: ViewGroup,
|
||||
container: ViewGroup,
|
||||
private val expandDialog: () -> Unit,
|
||||
interactor: TabHistoryViewInteractor
|
||||
) : LayoutContainer {
|
||||
|
||||
override val containerView: View?
|
||||
get() = container
|
||||
|
||||
val view: View = LayoutInflater.from(container.context)
|
||||
override val containerView: View = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_tabhistory, container, true)
|
||||
|
||||
private val adapter = TabHistoryAdapter(interactor)
|
||||
private val layoutManager = object : LinearLayoutManager(view.context) {
|
||||
private val layoutManager = object : LinearLayoutManager(containerView.context) {
|
||||
override fun onLayoutCompleted(state: RecyclerView.State?) {
|
||||
super.onLayoutCompleted(state)
|
||||
currentIndex?.let { index ->
|
||||
|
@ -73,7 +70,7 @@ class TabHistoryView(
|
|||
isSelected = index == historyState.currentIndex
|
||||
)
|
||||
}
|
||||
adapter.historyList = items
|
||||
adapter.submitList(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,30 +5,37 @@
|
|||
package org.mozilla.fenix.tabhistory
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.text.bold
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.history_list_item.view.*
|
||||
import kotlinx.android.synthetic.main.tab_history_list_item.*
|
||||
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.library.LibrarySiteItemView
|
||||
import org.mozilla.fenix.utils.view.ViewHolder
|
||||
|
||||
class TabHistoryViewHolder(
|
||||
private val view: View,
|
||||
view: View,
|
||||
private val interactor: TabHistoryViewInteractor
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
) : ViewHolder(view) {
|
||||
|
||||
private lateinit var item: TabHistoryItem
|
||||
|
||||
init {
|
||||
history_layout.setOnClickListener { interactor.goToHistoryItem(item) }
|
||||
}
|
||||
|
||||
fun bind(item: TabHistoryItem) {
|
||||
view.history_layout.overflowView.isVisible = false
|
||||
view.history_layout.urlView.text = item.url
|
||||
view.history_layout.loadFavicon(item.url)
|
||||
this.item = item
|
||||
|
||||
view.history_layout.titleView.text = if (item.isSelected) {
|
||||
buildSpannedString {
|
||||
bold { append(item.title) }
|
||||
}
|
||||
history_layout.displayAs(LibrarySiteItemView.ItemType.SITE)
|
||||
history_layout.overflowView.isVisible = false
|
||||
history_layout.titleView.text = item.title
|
||||
history_layout.urlView.text = item.url
|
||||
history_layout.loadFavicon(item.url)
|
||||
|
||||
if (item.isSelected) {
|
||||
history_layout.setBackgroundColor(history_layout.context.getColorFromAttr(R.attr.tabHistoryItemSelectedBackground))
|
||||
} else {
|
||||
item.title
|
||||
history_layout.background = null
|
||||
}
|
||||
|
||||
view.setOnClickListener { interactor.goToHistoryItem(item) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ package org.mozilla.fenix.tabtray
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -19,7 +20,8 @@ import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.ViewHolder
|
|||
* multiple [RecyclerView.Adapter] in one [RecyclerView].
|
||||
*/
|
||||
class SaveToCollectionsButtonAdapter(
|
||||
private val interactor: TabTrayInteractor
|
||||
private val interactor: TabTrayInteractor,
|
||||
private val isPrivate: Boolean = false
|
||||
) : ListAdapter<Item, ViewHolder>(DiffCallback) {
|
||||
|
||||
init {
|
||||
|
@ -31,7 +33,26 @@ class SaveToCollectionsButtonAdapter(
|
|||
return ViewHolder(itemView, interactor)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) = Unit
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.isNullOrEmpty()) {
|
||||
onBindViewHolder(holder, position)
|
||||
return
|
||||
}
|
||||
|
||||
when (val change = payloads[0]) {
|
||||
is TabTrayView.TabChange -> {
|
||||
holder.itemView.isVisible = change == TabTrayView.TabChange.NORMAL
|
||||
}
|
||||
is MultiselectModeChange -> {
|
||||
holder.itemView.isVisible = change == MultiselectModeChange.NORMAL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.itemView.isVisible = !isPrivate &&
|
||||
interactor.onModeRequested() is TabTrayDialogFragmentState.Mode.Normal
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return ViewHolder.LAYOUT_ID
|
||||
|
@ -43,6 +64,10 @@ class SaveToCollectionsButtonAdapter(
|
|||
override fun areContentsTheSame(oldItem: Item, newItem: Item) = true
|
||||
}
|
||||
|
||||
enum class MultiselectModeChange {
|
||||
MULTISELECT, NORMAL
|
||||
}
|
||||
|
||||
/**
|
||||
* An object to identify the data type.
|
||||
*/
|
||||
|
|
|
@ -8,13 +8,15 @@ import androidx.annotation.VisibleForTesting
|
|||
import androidx.navigation.NavController
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import mozilla.components.browser.session.Session
|
||||
import mozilla.components.browser.session.SessionManager
|
||||
import mozilla.components.concept.engine.profiler.Profiler
|
||||
import mozilla.components.concept.engine.prompt.ShareData
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
||||
import org.mozilla.fenix.components.TabCollectionStorage
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.sessionsOfType
|
||||
import org.mozilla.fenix.home.HomeFragment
|
||||
|
||||
|
@ -41,7 +43,9 @@ interface TabTrayController {
|
|||
/**
|
||||
* Default behavior of [TabTrayController]. Other implementations are possible.
|
||||
*
|
||||
* @param activity [HomeActivity] used for context and other Android interactions.
|
||||
* @param profiler [Profiler] used for profiling.
|
||||
* @param sessionManager [HomeActivity] used for retrieving a list of sessions.
|
||||
* @param browsingModeManager [HomeActivity] used for registering browsing mode.
|
||||
* @param navController [NavController] used for navigation.
|
||||
* @param dismissTabTray callback allowing to request this entire Fragment to be dismissed.
|
||||
* @param tabTrayDialogFragmentStore [TabTrayDialogFragmentStore] holding the State for all Views displayed
|
||||
|
@ -55,7 +59,10 @@ interface TabTrayController {
|
|||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class DefaultTabTrayController(
|
||||
private val activity: HomeActivity,
|
||||
private val profiler: Profiler?,
|
||||
private val sessionManager: SessionManager,
|
||||
private val browsingModeManager: BrowsingModeManager,
|
||||
private val tabCollectionStorage: TabCollectionStorage,
|
||||
private val navController: NavController,
|
||||
private val dismissTabTray: () -> Unit,
|
||||
private val dismissTabTrayAndNavigateHome: (String) -> Unit,
|
||||
|
@ -65,14 +72,13 @@ class DefaultTabTrayController(
|
|||
private val showChooseCollectionDialog: (List<Session>) -> Unit,
|
||||
private val showAddNewCollectionDialog: (List<Session>) -> Unit
|
||||
) : TabTrayController {
|
||||
private val tabCollectionStorage = activity.components.core.tabCollectionStorage
|
||||
|
||||
override fun onNewTabTapped(private: Boolean) {
|
||||
val startTime = activity.components.core.engine.profiler?.getProfilerTime()
|
||||
activity.browsingModeManager.mode = BrowsingMode.fromBoolean(private)
|
||||
val startTime = profiler?.getProfilerTime()
|
||||
browsingModeManager.mode = BrowsingMode.fromBoolean(private)
|
||||
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
|
||||
dismissTabTray()
|
||||
activity.components.core.engine.profiler?.addMarker(
|
||||
profiler?.addMarker(
|
||||
"DefaultTabTrayController.onNewTabTapped",
|
||||
startTime
|
||||
)
|
||||
|
@ -84,7 +90,7 @@ class DefaultTabTrayController(
|
|||
|
||||
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
|
||||
val sessionList = selectedTabs.map {
|
||||
activity.components.core.sessionManager.findSessionById(it.id) ?: return
|
||||
sessionManager.findSessionById(it.id) ?: return
|
||||
}
|
||||
|
||||
// Only register the observer right before moving to collection creation
|
||||
|
@ -141,7 +147,7 @@ class DefaultTabTrayController(
|
|||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
private fun getListOfSessions(private: Boolean): List<Session> {
|
||||
return activity.components.core.sessionManager.sessionsOfType(private = private).toList()
|
||||
return sessionManager.sessionsOfType(private = private).toList()
|
||||
}
|
||||
|
||||
override fun onModeRequested(): TabTrayDialogFragmentState.Mode {
|
||||
|
|
|
@ -163,7 +163,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
|
|||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate
|
||||
val activity = activity as HomeActivity
|
||||
val isPrivate = activity.browsingModeManager.mode.isPrivate
|
||||
|
||||
val thumbnailLoader = ThumbnailLoader(requireContext().components.core.thumbnailStorage)
|
||||
val adapter = FenixTabsAdapter(requireContext(), thumbnailLoader)
|
||||
|
@ -173,7 +174,10 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
|
|||
adapter,
|
||||
interactor = TabTrayFragmentInteractor(
|
||||
DefaultTabTrayController(
|
||||
activity = (activity as HomeActivity),
|
||||
profiler = activity.components.core.engine.profiler,
|
||||
sessionManager = activity.components.core.sessionManager,
|
||||
browsingModeManager = activity.browsingModeManager,
|
||||
tabCollectionStorage = activity.components.core.tabCollectionStorage,
|
||||
navController = findNavController(),
|
||||
dismissTabTray = ::dismissAllowingStateLoss,
|
||||
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
|
||||
|
|
|
@ -41,6 +41,7 @@ import org.mozilla.fenix.R
|
|||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.MultiselectModeChange
|
||||
|
||||
/**
|
||||
* View that contains and configures the BrowserAwesomeBar
|
||||
|
@ -70,8 +71,9 @@ class TabTrayView(
|
|||
private val tabTrayItemMenu: TabTrayItemMenu
|
||||
private var menu: BrowserMenu? = null
|
||||
|
||||
private val bottomSheetCallback: BottomSheetBehavior.BottomSheetCallback
|
||||
private var tabsTouchHelper: TabsTouchHelper
|
||||
private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor)
|
||||
private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate)
|
||||
|
||||
private var hasLoaded = false
|
||||
|
||||
|
@ -83,7 +85,7 @@ class TabTrayView(
|
|||
|
||||
toggleFabText(isPrivate)
|
||||
|
||||
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
if (!hasAccessibilityEnabled) {
|
||||
if (slideOffset >= SLIDE_OFFSET) {
|
||||
|
@ -100,7 +102,9 @@ class TabTrayView(
|
|||
interactor.onTabTrayDismissed()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
behavior.addBottomSheetCallback(bottomSheetCallback)
|
||||
|
||||
val selectedTabIndex = if (!isPrivate) {
|
||||
DEFAULT_TAB_ID
|
||||
|
@ -230,9 +234,21 @@ class TabTrayView(
|
|||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
||||
enum class TabChange {
|
||||
PRIVATE, NORMAL
|
||||
}
|
||||
|
||||
private fun toggleSaveToCollectionButton(isPrivate: Boolean) {
|
||||
collectionsButtonAdapter.notifyItemChanged(
|
||||
0,
|
||||
if (isPrivate) TabChange.PRIVATE else TabChange.NORMAL
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
toggleFabText(isPrivateModeSelected)
|
||||
filterTabs.invoke(isPrivateModeSelected)
|
||||
toggleSaveToCollectionButton(isPrivateModeSelected)
|
||||
|
||||
updateUINormalMode(view.context.components.core.store.state)
|
||||
scrollToTab(view.context.components.core.store.state.selectedTabId)
|
||||
|
@ -257,7 +273,7 @@ class TabTrayView(
|
|||
val oldMode = mode
|
||||
|
||||
if (oldMode::class != state.mode::class) {
|
||||
updateTabsForModeChanged()
|
||||
updateTabsForMultiselectModeChanged(state.mode is TabTrayDialogFragmentState.Mode.MultiSelect)
|
||||
if (view.context.settings().accessibilityServicesEnabled) {
|
||||
view.announceForAccessibility(
|
||||
if (state.mode == TabTrayDialogFragmentState.Mode.Normal) view.context.getString(
|
||||
|
@ -273,6 +289,7 @@ class TabTrayView(
|
|||
view.tabsTray.apply {
|
||||
tabsTouchHelper.attachToRecyclerView(this)
|
||||
}
|
||||
behavior.addBottomSheetCallback(bottomSheetCallback)
|
||||
|
||||
toggleUIMultiselect(multiselect = false)
|
||||
|
||||
|
@ -281,6 +298,7 @@ class TabTrayView(
|
|||
is TabTrayDialogFragmentState.Mode.MultiSelect -> {
|
||||
// Disable swipe to delete while in multiselect
|
||||
tabsTouchHelper.attachToRecyclerView(null)
|
||||
behavior.removeBottomSheetCallback(bottomSheetCallback)
|
||||
|
||||
toggleUIMultiselect(multiselect = true)
|
||||
|
||||
|
@ -402,13 +420,18 @@ class TabTrayView(
|
|||
view.tab_layout.isVisible = !multiselect
|
||||
}
|
||||
|
||||
private fun updateTabsForModeChanged() {
|
||||
private fun updateTabsForMultiselectModeChanged(inMultiselectMode: Boolean) {
|
||||
view.tabsTray.apply {
|
||||
val tabs = view.context.components.core.store.state.getNormalOrPrivateTabs(
|
||||
isPrivateModeSelected
|
||||
)
|
||||
|
||||
this.adapter?.notifyItemRangeChanged(0, tabs.size, true)
|
||||
collectionsButtonAdapter.notifyItemChanged(
|
||||
0,
|
||||
if (inMultiselectMode) MultiselectModeChange.MULTISELECT else MultiselectModeChange.NORMAL
|
||||
)
|
||||
|
||||
tabsAdapter.notifyItemRangeChanged(0, tabs.size, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -420,7 +443,7 @@ class TabTrayView(
|
|||
|
||||
val selectedBrowserTabIndex = tabs.indexOfFirst { it.id == itemId }
|
||||
|
||||
this.adapter?.notifyItemChanged(
|
||||
tabsAdapter.notifyItemChanged(
|
||||
selectedBrowserTabIndex, true
|
||||
)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ import mozilla.components.feature.media.ext.playIfPaused
|
|||
import mozilla.components.support.base.observer.Observable
|
||||
import mozilla.components.support.images.ImageLoadRequest
|
||||
import mozilla.components.support.images.loader.ImageLoader
|
||||
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
|
@ -36,6 +35,7 @@ import org.mozilla.fenix.ext.increaseTapArea
|
|||
import org.mozilla.fenix.ext.removeAndDisable
|
||||
import org.mozilla.fenix.ext.removeTouchDelegate
|
||||
import org.mozilla.fenix.ext.showAndEnable
|
||||
import org.mozilla.fenix.ext.toShortUrl
|
||||
import org.mozilla.fenix.utils.Do
|
||||
import kotlin.math.max
|
||||
|
||||
|
@ -160,7 +160,9 @@ class TabTrayViewHolder(
|
|||
// is done in the toolbar and awesomebar:
|
||||
// https://github.com/mozilla-mobile/fenix/issues/1824
|
||||
// https://github.com/mozilla-mobile/android-components/issues/6985
|
||||
urlView?.text = tab.url.tryGetHostFromUrl().take(MAX_URI_LENGTH)
|
||||
urlView?.text = tab.url
|
||||
.toShortUrl(itemView.context.components.publicSuffixList)
|
||||
.take(MAX_URI_LENGTH)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -36,7 +36,6 @@ import org.mozilla.fenix.settings.PhoneFeature
|
|||
import org.mozilla.fenix.settings.deletebrowsingdata.DeleteBrowsingDataOnQuitType
|
||||
import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu
|
||||
import org.mozilla.fenix.settings.logins.SortingStrategy
|
||||
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsFragment
|
||||
import org.mozilla.fenix.settings.registerOnSharedPreferenceChangeListener
|
||||
import java.security.InvalidParameterException
|
||||
|
||||
|
@ -324,6 +323,15 @@ class Settings(private val appContext: Context) : PreferencesHolder {
|
|||
default = true
|
||||
)
|
||||
|
||||
/**
|
||||
* Caches the last known "is default browser" state when the app was paused.
|
||||
* For an up to do date state use `isDefaultBrowser` instead.
|
||||
*/
|
||||
var wasDefaultBrowserOnLastPause by booleanPreference(
|
||||
appContext.getPreferenceKey(R.string.pref_key_default_browser),
|
||||
default = isDefaultBrowser()
|
||||
)
|
||||
|
||||
fun isDefaultBrowser(): Boolean {
|
||||
val browsers = BrowsersCache.all(appContext)
|
||||
return browsers.isDefaultBrowser
|
||||
|
@ -811,36 +819,26 @@ class Settings(private val appContext: Context) : PreferencesHolder {
|
|||
|
||||
private var savedLoginsSortingStrategyString by stringPreference(
|
||||
appContext.getPreferenceKey(R.string.pref_key_saved_logins_sorting_strategy),
|
||||
default = SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY
|
||||
default = SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort.strategyString
|
||||
)
|
||||
|
||||
val savedLoginsMenuHighlightedItem: SavedLoginsSortingStrategyMenu.Item
|
||||
get() {
|
||||
return when (savedLoginsSortingStrategyString) {
|
||||
SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY -> {
|
||||
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort
|
||||
}
|
||||
SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> {
|
||||
SavedLoginsSortingStrategyMenu.Item.LastUsedSort
|
||||
}
|
||||
else -> SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort
|
||||
}
|
||||
}
|
||||
get() = SavedLoginsSortingStrategyMenu.Item.fromString(savedLoginsSortingStrategyString)
|
||||
|
||||
var savedLoginsSortingStrategy: SortingStrategy
|
||||
get() {
|
||||
return when (savedLoginsSortingStrategyString) {
|
||||
SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY -> SortingStrategy.Alphabetically(
|
||||
appContext.components.publicSuffixList
|
||||
)
|
||||
SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> SortingStrategy.LastUsed
|
||||
else -> SortingStrategy.Alphabetically(appContext.components.publicSuffixList)
|
||||
return when (savedLoginsMenuHighlightedItem) {
|
||||
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort ->
|
||||
SortingStrategy.Alphabetically(appContext.components.publicSuffixList)
|
||||
SavedLoginsSortingStrategyMenu.Item.LastUsedSort -> SortingStrategy.LastUsed
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
savedLoginsSortingStrategyString = when (value) {
|
||||
is SortingStrategy.Alphabetically -> SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY
|
||||
is SortingStrategy.LastUsed -> SavedLoginsFragment.SORTING_STRATEGY_LAST_USED
|
||||
is SortingStrategy.Alphabetically ->
|
||||
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort.strategyString
|
||||
is SortingStrategy.LastUsed ->
|
||||
SavedLoginsSortingStrategyMenu.Item.LastUsedSort.strategyString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,11 @@ object ToolbarPopupWindow {
|
|||
copyVisible: Boolean = true
|
||||
) {
|
||||
val context = view.get()?.context ?: return
|
||||
val isCustomTabSession = customTabSession != null
|
||||
val clipboard = context.components.clipboardHandler
|
||||
if (!copyVisible && clipboard.text.isNullOrEmpty()) return
|
||||
|
||||
val isCustomTabSession = customTabSession != null
|
||||
|
||||
val customView = LayoutInflater.from(context)
|
||||
.inflate(R.layout.browser_toolbar_popup_window, null)
|
||||
val popupWindow = PopupWindow(
|
||||
|
|
|
@ -23,6 +23,8 @@ import androidx.core.graphics.drawable.toBitmap
|
|||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.IntentReceiverActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.metrics
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
|
||||
import org.mozilla.fenix.widget.VoiceSearchActivity
|
||||
|
@ -36,6 +38,7 @@ class SearchWidgetProvider : AppWidgetProvider() {
|
|||
|
||||
override fun onEnabled(context: Context) {
|
||||
context.settings().addSearchWidgetInstalled(1)
|
||||
context.metrics.track(Event.SearchWidgetInstalled)
|
||||
}
|
||||
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
|
|
|
@ -9,5 +9,5 @@
|
|||
android:width="14dp"
|
||||
android:height="14dp" />
|
||||
<solid android:color="?accentBright" />
|
||||
<stroke android:color="@color/light_grey_05"/>
|
||||
<stroke android:color="@color/photonLightGrey05"/>
|
||||
</shape>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -7,6 +7,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/fragment_browser" />
|
||||
|
||||
<org.mozilla.fenix.browser.TabPreview
|
||||
android:id="@+id/tabPreview"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -45,5 +45,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
tools:listitem="@layout/exception_item" />
|
||||
tools:listheader="@layout/exceptions_description"
|
||||
tools:listitem="@layout/exception_item"
|
||||
tools:listfooter="@layout/delete_exceptions_button" />
|
||||
</FrameLayout>
|
||||
|
|
|
@ -20,20 +20,6 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sync_tabs_status"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/sync_connect_device"
|
||||
android:textColor="?secondaryText"
|
||||
android:textSize="16sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/synced_tabs_pull_to_refresh"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/removeAllExceptions"
|
||||
style="@style/DestructiveButton"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/exceptions_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="12dp"
|
||||
android:text="@string/enhanced_tracking_protection_exceptions"
|
||||
tools:text="@string/enhanced_tracking_protection_exceptions"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="16sp" />
|
||||
|
|
|
@ -12,27 +12,14 @@
|
|||
android:focusable="true"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/favicon_wrapper"
|
||||
android:layout_width="@dimen/history_favicon_width_height"
|
||||
android:layout_height="@dimen/history_favicon_width_height"
|
||||
android:padding="@dimen/saved_logins_item_padding"
|
||||
android:layout_marginStart="@dimen/saved_logins_item_margin_start"
|
||||
android:background="@drawable/top_sites_background"
|
||||
android:layout_gravity="center"
|
||||
android:importantForAccessibility="noHideDescendants"
|
||||
<ImageView
|
||||
android:id="@+id/favicon_image"
|
||||
style="@style/Mozac.Widgets.Favicon"
|
||||
android:layout_marginStart="16dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
<ImageView
|
||||
android:id="@+id/favicon_image"
|
||||
android:layout_width="@dimen/preference_icon_drawable_size"
|
||||
android:layout_height="@dimen/preference_icon_drawable_size"
|
||||
android:layout_gravity="center"
|
||||
android:adjustViewBounds="true"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="fitCenter" />
|
||||
</FrameLayout>
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/webAddressView"
|
||||
|
@ -46,7 +33,7 @@
|
|||
android:textColor="?primaryText"
|
||||
app:layout_constraintBottom_toTopOf="@id/usernameView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/favicon_wrapper"
|
||||
app:layout_constraintStart_toEndOf="@+id/favicon_image"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="mozilla.org" />
|
||||
|
@ -62,7 +49,7 @@
|
|||
android:textColor="?secondaryText"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/favicon_wrapper"
|
||||
app:layout_constraintStart_toEndOf="@+id/favicon_image"
|
||||
app:layout_constraintTop_toBottomOf="@id/webAddressView"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="mozilla.org" />
|
||||
|
|
|
@ -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" />
|
|
@ -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
Loading…
Reference in New Issue