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