1
0
Fork 0

Copione merged onto master
continuous-integration/drone/push Build is passing Details

master
blallo 2020-06-26 00:00:29 +02:00
commit a8e16ed074
91 changed files with 1597 additions and 861 deletions

View File

@ -67,6 +67,8 @@ Note: Both Android SDK and NDK are required.
```
Use app:assembleGeckoNightlyDebug to build with the Gecko Nightly version instead.
If this errors out, make sure that you have an `ANDROID_SDK_ROOT` environment
variable pointing to the right path.
3. Make sure to select the correct build variant in Android Studio. See the next section.

View File

@ -53,6 +53,7 @@ android {
shrinkResources false
minifyEnabled false
applicationIdSuffix ".fenix.debug"
buildConfigField "String", "AMO_COLLECTION", "\"3204bb44a6ef44d39ee34917f28055\""
manifestPlaceholders.isRaptorEnabled = "true"
resValue "bool", "IS_DEBUG", "true"
pseudoLocalesEnabled true
@ -72,6 +73,7 @@ android {
fenixNightly releaseTemplate >> {
applicationIdSuffix ".fenix.nightly"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
buildConfigField "String", "AMO_COLLECTION", "\"3204bb44a6ef44d39ee34917f28055\""
def deepLinkSchemeValue = "fenix-nightly"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = ["deepLinkScheme": deepLinkSchemeValue]
@ -129,6 +131,7 @@ android {
applicationIdSuffix ".fennec_aurora"
def deepLinkSchemeValue = "fenix-nightly"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
buildConfigField "String", "AMO_COLLECTION", "\"3204bb44a6ef44d39ee34917f28055\""
manifestPlaceholders = [
// This release type is meant to replace Firefox (Release channel) and therefore needs to inherit
// its sharedUserId for all eternity. See:
@ -503,6 +506,7 @@ dependencies {
implementation Deps.mozilla_feature_webnotifications
implementation Deps.mozilla_feature_webcompat_reporter
implementation Deps.mozilla_service_digitalassetlinks
implementation Deps.mozilla_service_experiments
implementation Deps.mozilla_service_sync_logins
implementation Deps.mozilla_service_firefox_accounts

View File

@ -205,6 +205,131 @@ events:
- fenix-core@mozilla.com
expires: "2020-09-01"
onboarding:
fxa_auto_signin:
type: event
description:
The onboarding automatic sign in card was tapped.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/10824
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11867
notification_emails:
- fenix-core@mozilla.com
- erichards@mozilla.com
expires: "2020-09-01"
fxa_manual_signin:
type: event
description:
The onboarding manual sign in card was tapped.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/10824
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11867
notification_emails:
- fenix-core@mozilla.com
- erichards@mozilla.com
expires: "2020-09-01"
privacy_notice:
type: event
description:
The onboarding privacy notice card was tapped.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/10824
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11867
notification_emails:
- fenix-core@mozilla.com
- erichards@mozilla.com
expires: "2020-09-01"
pref_toggled_private_browsing:
type: event
description:
The private browsing preference was selected from the onboarding card.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/10824
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11867
notification_emails:
- fenix-core@mozilla.com
- erichards@mozilla.com
expires: "2020-09-01"
pref_toggled_toolbar_position:
type: event
description:
The toolbar position preference was chosen from the onboarding card.
extra_keys:
position:
description: |
A string that indicates the position of the toolbar TOP or BOTTOM.
Default: BOTTOM
bugs:
- https://github.com/mozilla-mobile/fenix/issues/10824
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11867
notification_emails:
- fenix-core@mozilla.com
- erichards@mozilla.com
expires: "2020-09-01"
pref_toggled_tracking_prot:
type: event
description:
The tracking protection preference was chosen from the onboarding card.
extra_keys:
position:
description: |
A string that indicates the Tracking Protection policy STANDARD
or STRICT. Default: Toggle ON, STANDARD
bugs:
- https://github.com/mozilla-mobile/fenix/issues/10824
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11867
notification_emails:
- fenix-core@mozilla.com
- erichards@mozilla.com
expires: "2020-09-01"
whats_new:
type: event
description:
The onboarding What\'s New card was tapped.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/10824
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11867
notification_emails:
- fenix-core@mozilla.com
- erichards@mozilla.com
expires: "2020-09-01"
pref_toggled_theme_picker:
type: event
description:
The device theme was chosen using the theme picker onboarding card.
extra_keys:
theme:
description: |
A string that indicates the theme LIGHT, DARK, or FOLLOW DEVICE.
Default: FOLLOW DEVICE
bugs:
- https://github.com/mozilla-mobile/fenix/issues/10824
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11867
notification_emails:
- fenix-core@mozilla.com
- erichards@mozilla.com
expires: "2020-09-01"
finish:
type: event
description:
The user taps starts browsing and ends the onboarding experience.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/10824
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11867
notification_emails:
- fenix-core@mozilla.com
- erichards@mozilla.com
expires: "2020-09-01"
search_shortcuts:
selected:
type: event
@ -1741,6 +1866,57 @@ private_browsing_mode:
- fenix-core@mozilla.com
expires: "2020-09-01"
contextual_hint.tracking_protection:
display:
type: event
description: |
The enhanced tracking protection contextual hint was
displayed.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/9625
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11923
notification_emails:
- fenix-core@mozilla.com
expires: "2020-09-01"
dismiss:
type: event
description: |
The enhanced tracking protection contextual hint was
dismissed
by pressing the close button
bugs:
- https://github.com/mozilla-mobile/fenix/issues/9625
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11923
notification_emails:
- fenix-core@mozilla.com
expires: "2020-09-01"
outside_tap:
type: event
description: |
The user tapped outside of the etp contextual hint
(which has no effect).
bugs:
- https://github.com/mozilla-mobile/fenix/issues/9625
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11923
notification_emails:
- fenix-core@mozilla.com
expires: "2020-09-01"
inside_tap:
type: event
description: |
The user tapped inside of the etp contextual hint
(which brings up the etp panel for this site).
bugs:
- https://github.com/mozilla-mobile/fenix/issues/9625
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11923
notification_emails:
- fenix-core@mozilla.com
expires: "2020-09-01"
tracking_protection:
exception_added:
type: event
@ -2319,11 +2495,11 @@ pocket:
- fenix-core@mozilla.com
expires: "2020-09-01"
installation:
first_session:
campaign:
type: string
send_in_pings:
- installation
- first-session
description: |
The name of the campaign that is responsible for this installation.
bugs:
@ -2336,7 +2512,7 @@ installation:
network:
type: string
send_in_pings:
- installation
- first-session
description: |
The name of the Network that sourced this installation.
bugs:
@ -2349,7 +2525,7 @@ installation:
adgroup:
type: string
send_in_pings:
- installation
- first-session
description: |
The name of the AdGroup that was used to source this installation.
bugs:
@ -2361,7 +2537,7 @@ installation:
expires: "2020-09-01"
creative:
send_in_pings:
- installation
- first-session
type: string
description: |
The identifier of the creative material that the user interacted with.
@ -2374,7 +2550,7 @@ installation:
expires: "2020-09-01"
timestamp:
send_in_pings:
- installation
- first-session
type: datetime
description: |
The Glean generated date and time of the installation. This is

View File

@ -19,9 +19,10 @@ activation:
notification_emails:
- fenix-core@mozilla.com
installation:
first-session:
description: |
This ping is intended to capture the source of the installation
This ping is intended to capture the source of the app install
on the first session.
include_client_id: true
bugs:
- https://github.com/mozilla-mobile/fenix/issues/7295

View File

@ -204,7 +204,6 @@ class TabbedBrowsingTest {
}
}
@Ignore("Temp disabled, intermittent test: https://github.com/mozilla-mobile/fenix/issues/9783")
@Test
fun closePrivateTabsNotificationTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -222,7 +221,7 @@ class TabbedBrowsingTest {
}.clickClosePrivateTabsNotification {
// Tap an empty spot on the app homescreen to make sure it's into focus
sendSingleTapToScreen(20, 20)
verifyPrivateSessionMessage()
verifyHomeScreen()
}
}
}

View File

@ -9,6 +9,7 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@ -24,7 +25,6 @@ import org.mozilla.fenix.ui.robots.homeScreen
class ThreeDotMenuMainTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private lateinit var mockWebServer: MockWebServer
@get:Rule
@ -38,6 +38,16 @@ class ThreeDotMenuMainTest {
}
}
// changing the device preference for Touch and Hold delay, to avoid long-clicks instead of a single-click
companion object {
@BeforeClass
@JvmStatic
fun setDevicePreference() {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.executeShellCommand("settings put secure long_press_timeout 1500")
}
}
@After
fun tearDown() {
mockWebServer.shutdown()

View File

@ -19,7 +19,6 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.toPackage
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
@ -369,19 +368,19 @@ private fun assertLeakCanaryButton() {
private fun assertAboutHeading(): ViewInteraction {
scrollToElementByText("About")
return onView(withText("About"))
.check(matches(isCompletelyDisplayed()))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertRateOnGooglePlay(): ViewInteraction {
scrollToElementByText("About Firefox Preview")
return onView(withText("Rate on Google Play"))
.check(matches(isCompletelyDisplayed()))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAboutFirefoxPreview(): ViewInteraction {
scrollToElementByText("About Firefox Preview")
return onView(withText("About Firefox Preview"))
.check(matches(isCompletelyDisplayed()))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
fun swipeToBottom() = onView(withId(R.id.recycler_view)).perform(ViewActions.swipeUp())

View File

@ -43,7 +43,6 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.push.PushFxaIntegration
import org.mozilla.fenix.push.WebPushEngineIntegration
import org.mozilla.fenix.session.NotificationSessionObserver
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
import org.mozilla.fenix.session.VisibilityLifecycleCallback
import org.mozilla.fenix.utils.BrowsersCache
@ -157,9 +156,6 @@ open class FenixApplication : LocaleAwareApplication() {
visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService())
registerActivityLifecycleCallbacks(visibilityLifecycleCallback)
val privateNotificationObserver = NotificationSessionObserver(this)
privateNotificationObserver.start()
// Storage maintenance disabled, for now, as it was interfering with background migrations.
// See https://github.com/mozilla-mobile/fenix/issues/7227 for context.
// if ((System.currentTimeMillis() - settings().lastPlacesStorageMaintenance) > ONE_DAY_MILLIS) {

View File

@ -74,6 +74,7 @@ import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.search.SearchFragmentDirections
import org.mozilla.fenix.session.NotificationSessionObserver
import org.mozilla.fenix.settings.SettingsFragmentDirections
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
import org.mozilla.fenix.settings.about.AboutFragmentDirections
@ -106,6 +107,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
private var isVisuallyComplete = false
private var visualCompletenessQueue: RunWhenReadyQueue? = null
private var privateNotificationObserver: NotificationSessionObserver? = null
private var isToolbarInflated = false
@ -154,6 +156,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
sessionObserver = UriOpenedObserver(this)
checkPrivateShortcutEntryPoint(intent)
privateNotificationObserver = NotificationSessionObserver(applicationContext).also {
it.start()
}
if (isActivityColdStarted(intent, savedInstanceState)) {
externalSourceIntentProcessors.any { it.process(intent, navHost.navController, this.intent) }
}
@ -176,6 +183,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
StartupTimeline.homeActivityLifecycleObserver
)
StartupTimeline.onActivityCreateEndHome(this)
if (shouldAddToRecentsScreen(intent)) {
intent.removeExtra(START_IN_RECENTS_SCREEN)
moveTaskToBack(true)
}
}
@CallSuper
@ -208,6 +220,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
BrowsersCache.resetAll()
}
override fun onDestroy() {
super.onDestroy()
privateNotificationObserver?.stop()
}
/**
* Handles intents received when the activity is open.
*/
@ -313,6 +330,30 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
return settings().lastKnownMode
}
/**
* Determines whether the activity should be pushed to be backstack (i.e., 'minimized' to the recents
* screen) upon starting.
* @param intent - The intent that started this activity. Is checked for having the 'START_IN_RECENTS_SCREEN'-extra.
* @return true if the activity should be started and pushed to the recents screen, false otherwise.
*/
private fun shouldAddToRecentsScreen(intent: Intent?): Boolean {
intent?.toSafeIntent()?.let {
return it.getBooleanExtra(START_IN_RECENTS_SCREEN, false)
}
return false
}
private fun checkPrivateShortcutEntryPoint(intent: Intent) {
if (intent.hasExtra(OPEN_TO_SEARCH) &&
(intent.getStringExtra(OPEN_TO_SEARCH) ==
StartSearchIntentProcessor.STATIC_SHORTCUT_NEW_PRIVATE_TAB ||
intent.getStringExtra(OPEN_TO_SEARCH) ==
StartSearchIntentProcessor.PRIVATE_BROWSING_PINNED_SHORTCUT)
) {
NotificationSessionObserver.isStartedFromPrivateShortcut = true
}
}
private fun setupThemeAndBrowsingMode(mode: BrowsingMode) {
settings().lastKnownMode = mode
browsingModeManager = createBrowsingModeManager(mode)
@ -499,5 +540,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
const val EXTRA_DELETE_PRIVATE_TABS = "notification_delete_and_open"
const val EXTRA_OPENED_FROM_NOTIFICATION = "notification_open"
const val delay = 5000L
const val START_IN_RECENTS_SCREEN = "start_in_recents_screen"
}
}

View File

@ -23,7 +23,6 @@ import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.android.synthetic.main.fragment_installed_add_on_details.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -128,9 +127,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
protected val browserToolbarView: BrowserToolbarView
get() = _browserToolbarView!!
private val sessionManager: SessionManager
get() = requireComponents.core.sessionManager
protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
@ -531,14 +527,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
)
session.register(observer = object : Session.Observer {
override fun onNavigationStateChanged(
session: Session,
canGoBack: Boolean,
canGoForward: Boolean
) {
// Once https://bugzilla.mozilla.org/show_bug.cgi?id=1626338 is fixed, we can
// rely solely on `onLoadRequest` entirely, but as it stands that is not called
// for history navigation (back or forward).
override fun onUrlChanged(session: Session, url: String) {
browserToolbarView.expand()
}
@ -813,9 +802,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
protected open fun removeSessionIfNeeded(): Boolean {
getSessionById()?.let { session ->
val sessionManager = requireComponents.core.sessionManager
if (session.source == Session.Source.ACTION_VIEW) {
return if (session.source == Session.Source.ACTION_VIEW) {
activity?.finish()
sessionManager.remove(session)
true
} else {
val isLastSession =
sessionManager.sessionsOfType(private = session.private).count() == 1
@ -823,7 +813,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
sessionManager.remove(session, true)
}
val goToOverview = isLastSession || !session.hasParentSession
return !goToOverview
!goToOverview
}
}
return false

View File

@ -8,14 +8,14 @@ package org.mozilla.fenix.collections
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.feature.tab.collections.TabCollection
import org.mozilla.fenix.components.Analytics
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.home.Tab
interface CollectionCreationController {
@ -65,10 +65,10 @@ fun List<Tab>.toSessionBundle(sessionManager: SessionManager): List<Session> {
class DefaultCollectionCreationController(
private val store: CollectionCreationStore,
private val dismiss: () -> Unit,
private val analytics: Analytics,
private val metrics: MetricController,
private val tabCollectionStorage: TabCollectionStorage,
private val sessionManager: SessionManager,
private val viewLifecycleScope: CoroutineScope
private val scope: CoroutineScope
) : CollectionCreationController {
companion object {
@ -79,26 +79,31 @@ class DefaultCollectionCreationController(
override fun saveCollectionName(tabs: List<Tab>, name: String) {
dismiss()
val sessionBundle = tabs.toList().toSessionBundle(sessionManager)
viewLifecycleScope.launch(Dispatchers.IO) {
val sessionBundle = tabs.toSessionBundle(sessionManager)
scope.launch(IO) {
tabCollectionStorage.createCollection(name, sessionBundle)
}
analytics.metrics.track(
metrics.track(
Event.CollectionSaved(normalSessionSize(sessionManager), sessionBundle.size)
)
}
override fun renameCollection(collection: TabCollection, name: String) {
dismiss()
viewLifecycleScope.launch(Dispatchers.IO) {
scope.launch(IO) {
tabCollectionStorage.renameCollection(collection, name)
analytics.metrics.track(Event.CollectionRenamed)
}
metrics.track(Event.CollectionRenamed)
}
override fun backPressed(fromStep: SaveCollectionStep) {
handleBackPress(fromStep)
val newStep = stepBack(fromStep)
if (newStep != null) {
store.dispatch(CollectionCreationAction.StepChanged(newStep))
} else {
dismiss()
}
}
override fun selectAllTabs() {
@ -116,12 +121,12 @@ class DefaultCollectionCreationController(
override fun selectCollection(collection: TabCollection, tabs: List<Tab>) {
dismiss()
val sessionBundle = tabs.toList().toSessionBundle(sessionManager)
viewLifecycleScope.launch(Dispatchers.IO) {
scope.launch(IO) {
tabCollectionStorage
.addTabsToCollection(collection, sessionBundle)
}
analytics.metrics.track(
metrics.track(
Event.CollectionTabsAdded(normalSessionSize(sessionManager), sessionBundle.size)
)
}
@ -171,25 +176,15 @@ class DefaultCollectionCreationController(
store.dispatch(CollectionCreationAction.TabRemoved(tab))
}
private fun handleBackPress(backFromStep: SaveCollectionStep) {
val newStep = stepBack(backFromStep)
if (newStep != null) {
store.dispatch(CollectionCreationAction.StepChanged(newStep))
} else {
dismiss()
}
}
/**
* Will return the next valid state according to this diagram.
*
* Name Collection -> Select Collection -> Select Tabs -> (dismiss fragment) <- Rename Collection
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun stepBack(
backFromStep: SaveCollectionStep
): SaveCollectionStep? {
/*
Will return the next valid state according to this diagram.
Name Collection -> Select Collection -> Select Tabs -> (dismiss fragment) <- Rename Collection
*/
val tabCollectionCount = store.state.tabCollections.size
val tabCount = store.state.tabs.size

View File

@ -74,10 +74,10 @@ class CollectionCreationFragment : DialogFragment() {
DefaultCollectionCreationController(
collectionCreationStore,
::dismiss,
requireComponents.analytics,
requireComponents.analytics.metrics,
requireComponents.core.tabCollectionStorage,
requireComponents.core.sessionManager,
viewLifecycleOwner.lifecycleScope
scope = lifecycleScope
)
)
collectionCreationView = CollectionCreationView(

View File

@ -61,7 +61,7 @@ class Components(private val context: Context) {
core.sessionManager,
useCases.sessionUseCases,
useCases.searchUseCases,
core.client,
core.relationChecker,
core.customTabsStore,
migrationStore,
core.webAppManifestStorage

View File

@ -41,8 +41,11 @@ import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature
import mozilla.components.feature.webnotifications.WebNotificationFeature
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
import mozilla.components.lib.dataprotect.generateEncryptionKey
import mozilla.components.service.digitalassetlinks.RelationChecker
import mozilla.components.service.digitalassetlinks.api.DigitalAssetLinksApi
import mozilla.components.service.sync.logins.SyncableLoginsStorage
import org.mozilla.fenix.AppRequestInterceptor
import org.mozilla.fenix.BuildConfig.DIGITAL_ASSET_LINKS_TOKEN
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
@ -136,6 +139,13 @@ class Core(private val context: Context) {
*/
val customTabsStore by lazy { CustomTabsServiceStore() }
/**
* The [RelationChecker] checks Digital Asset Links relationships for Trusted Web Activities.
*/
val relationChecker: RelationChecker by lazy {
DigitalAssetLinksApi(client, DIGITAL_ASSET_LINKS_TOKEN)
}
/**
* The session manager component provides access to a centralized registry of
* all browser sessions (i.e. tabs). It is initialized here to persist and restore

View File

@ -6,7 +6,6 @@ package org.mozilla.fenix.components
import android.content.Context
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.fetch.Client
import mozilla.components.feature.customtabs.CustomTabIntentProcessor
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
import mozilla.components.feature.intent.processing.TabIntentProcessor
@ -15,9 +14,9 @@ import mozilla.components.feature.pwa.intent.TrustedWebActivityIntentProcessor
import mozilla.components.feature.pwa.intent.WebAppIntentProcessor
import mozilla.components.feature.search.SearchUseCases
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.service.digitalassetlinks.RelationChecker
import mozilla.components.support.migration.MigrationIntentProcessor
import mozilla.components.support.migration.state.MigrationStore
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.customtabs.FennecWebAppIntentProcessor
import org.mozilla.fenix.home.intent.FennecBookmarkShortcutsIntentProcessor
import org.mozilla.fenix.utils.Mockable
@ -31,7 +30,7 @@ class IntentProcessors(
private val sessionManager: SessionManager,
private val sessionUseCases: SessionUseCases,
private val searchUseCases: SearchUseCases,
private val httpClient: Client,
private val relationChecker: RelationChecker,
private val customTabsStore: CustomTabsServiceStore,
private val migrationStore: MigrationStore,
private val manifestStorage: ManifestStorage
@ -63,9 +62,8 @@ class IntentProcessors(
TrustedWebActivityIntentProcessor(
sessionManager = sessionManager,
loadUrlUseCase = sessionUseCases.loadUrl,
httpClient = httpClient,
packageManager = context.packageManager,
apiKey = BuildConfig.DIGITAL_ASSET_LINKS_TOKEN,
relationChecker = relationChecker,
store = customTabsStore
),
WebAppIntentProcessor(sessionManager, sessionUseCases.loadUrl, manifestStorage),

View File

@ -8,7 +8,6 @@ import android.content.Context
import mozilla.components.browser.search.SearchEngineManager
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.thumbnails.ThumbnailsUseCases
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
import mozilla.components.concept.engine.Engine
import mozilla.components.feature.app.links.AppLinksUseCases
@ -46,16 +45,6 @@ class UseCases(
*/
val tabsUseCases: TabsUseCases by lazy { TabsUseCases(sessionManager) }
/**
* Use cases that provide tab thumbnail integration.
*/
val thumbnailUseCases: ThumbnailsUseCases by lazy {
ThumbnailsUseCases(
store,
thumbnailStorage
)
}
/**
* Use cases that provide search engine integration.
*/

View File

@ -37,7 +37,7 @@ class AdjustMetricsService(private val application: Application) : MetricsServic
true
)
val installationPing = InstallationPing(application)
val installationPing = FirstSessionPing(application)
config.setOnAttributionChangedListener {
if (!it.network.isNullOrEmpty()) {

View File

@ -11,11 +11,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.GleanMetrics.Installation
import org.mozilla.fenix.GleanMetrics.FirstSession
import org.mozilla.fenix.GleanMetrics.Pings
import org.mozilla.fenix.ext.settings
class InstallationPing(private val context: Context) {
class FirstSessionPing(private val context: Context) {
private val prefs: SharedPreferences by lazy {
context.getSharedPreferences(
@ -56,15 +56,15 @@ class InstallationPing(private val context: Context) {
internal fun triggerPing() {
if (checkMetricsNotEmpty()) {
context.settings().also {
Installation.campaign.set(it.adjustCampaignId)
Installation.adgroup.set(it.adjustAdGroup)
Installation.creative.set(it.adjustCreative)
Installation.network.set(it.adjustNetwork)
Installation.timestamp.set()
FirstSession.campaign.set(it.adjustCampaignId)
FirstSession.adgroup.set(it.adjustAdGroup)
FirstSession.creative.set(it.adjustCreative)
FirstSession.network.set(it.adjustNetwork)
FirstSession.timestamp.set()
}
CoroutineScope(Dispatchers.IO).launch {
Pings.installation.submit()
Pings.firstSession.submit()
markAsTriggered()
}
}

View File

@ -16,11 +16,13 @@ import org.mozilla.fenix.GleanMetrics.BookmarksManagement
import org.mozilla.fenix.GleanMetrics.BrowserSearch
import org.mozilla.fenix.GleanMetrics.Collections
import org.mozilla.fenix.GleanMetrics.ContextMenu
import org.mozilla.fenix.GleanMetrics.ContextualHintTrackingProtection
import org.mozilla.fenix.GleanMetrics.CrashReporter
import org.mozilla.fenix.GleanMetrics.CustomTab
import org.mozilla.fenix.GleanMetrics.DownloadNotification
import org.mozilla.fenix.GleanMetrics.ErrorPage
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.Events.preferenceToggled
import org.mozilla.fenix.GleanMetrics.FindInPage
import org.mozilla.fenix.GleanMetrics.History
import org.mozilla.fenix.GleanMetrics.Logins
@ -29,6 +31,7 @@ import org.mozilla.fenix.GleanMetrics.MediaState
import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.Pings
import org.mozilla.fenix.GleanMetrics.Pocket
import org.mozilla.fenix.GleanMetrics.Onboarding
import org.mozilla.fenix.GleanMetrics.Preferences
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingMode
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut
@ -552,6 +555,52 @@ private val Event.wrapper: EventWrapper<*>?
{ Events.tabCounterMenuAction.record(it) },
{ Events.tabCounterMenuActionKeys.valueOf(it) }
)
is Event.OnboardingWhatsNew -> EventWrapper<NoExtraKeys>(
{ Onboarding.whatsNew.record(it) }
)
is Event.OnboardingPrivateBrowsing -> EventWrapper<NoExtraKeys>(
{ Onboarding.prefToggledPrivateBrowsing.record(it) }
)
is Event.OnboardingPrivacyNotice -> EventWrapper<NoExtraKeys>(
{ Onboarding.privacyNotice.record(it) }
)
is Event.OnboardingManualSignIn -> EventWrapper<NoExtraKeys>(
{ Onboarding.fxaManualSignin.record(it) }
)
is Event.OnboardingAutoSignIn -> EventWrapper<NoExtraKeys>(
{ Onboarding.fxaAutoSignin.record(it) }
)
is Event.OnboardingFinish -> EventWrapper<NoExtraKeys>(
{ Onboarding.finish.record(it) }
)
is Event.OnboardingTrackingProtection -> EventWrapper(
{ Onboarding.prefToggledTrackingProt.record(it) },
{ Onboarding.prefToggledTrackingProtKeys.valueOf(it) }
)
is Event.OnboardingThemePicker -> EventWrapper(
{ Onboarding.prefToggledThemePicker.record(it) },
{ Onboarding.prefToggledThemePickerKeys.valueOf(it) }
)
is Event.OnboardingToolbarPosition -> EventWrapper(
{ Onboarding.prefToggledToolbarPosition.record(it) },
{ Onboarding.prefToggledToolbarPositionKeys.valueOf(it) }
)
is Event.ContextualHintETPDisplayed -> EventWrapper<NoExtraKeys>(
{ ContextualHintTrackingProtection.display.record(it) }
)
is Event.ContextualHintETPDismissed -> EventWrapper<NoExtraKeys>(
{ ContextualHintTrackingProtection.dismiss.record(it) }
)
is Event.ContextualHintETPInsideTap -> EventWrapper<NoExtraKeys>(
{ ContextualHintTrackingProtection.insideTap.record(it) }
)
is Event.ContextualHintETPOutsideTap -> EventWrapper<NoExtraKeys>(
{ ContextualHintTrackingProtection.outsideTap.record(it) }
)
// Don't record other events in Glean:
is Event.AddBookmark -> null
@ -570,7 +619,7 @@ class GleanMetricsService(private val context: Context) : MetricsService {
private var initialized = false
private val activationPing = ActivationPing(context)
private val installationPing = InstallationPing(context)
private val installationPing = FirstSessionPing(context)
override fun start() {
logger.debug("Enabling Glean.")

View File

@ -172,8 +172,39 @@ sealed class Event {
object SearchWidgetCFRCanceled : Event()
object SearchWidgetCFRNotNowPressed : Event()
object SearchWidgetCFRAddWidgetPressed : Event()
object OnboardingAutoSignIn : Event()
object OnboardingManualSignIn : Event()
object OnboardingPrivacyNotice : Event()
object OnboardingPrivateBrowsing : Event()
object OnboardingWhatsNew : Event()
object OnboardingFinish : Event()
object ContextualHintETPDisplayed : Event()
object ContextualHintETPDismissed : Event()
object ContextualHintETPOutsideTap : Event()
object ContextualHintETPInsideTap : Event()
// Interaction events with extras
data class OnboardingToolbarPosition(val position: Position) : Event() {
enum class Position { TOP, BOTTOM }
override val extras: Map<ToolbarSettings.changedPositionKeys, String>?
get() = hashMapOf(ToolbarSettings.changedPositionKeys.position to position.name)
}
data class OnboardingTrackingProtection(val setting: Setting) : Event() {
enum class Setting { STRICT, STANDARD }
override val extras: Map<TrackingProtection.etpSettingChangedKeys, String>?
get() = hashMapOf(TrackingProtection.etpSettingChangedKeys.etpSetting to setting.name)
}
data class OnboardingThemePicker(val theme: Theme) : Event() {
enum class Theme { LIGHT, DARK, FOLLOW_DEVICE }
override val extras: Map<AppTheme.darkThemeSelectedKeys, String>?
get() = mapOf(AppTheme.darkThemeSelectedKeys.source to theme.name)
}
data class PreferenceToggled(
val preferenceKey: String,
@ -371,7 +402,7 @@ sealed class Event {
}
data class DarkThemeSelected(val source: Source) : Event() {
enum class Source { SETTINGS, ONBOARDING }
enum class Source { SETTINGS }
override val extras: Map<AppTheme.darkThemeSelectedKeys, String>?
get() = mapOf(AppTheme.darkThemeSelectedKeys.source to source.name)

View File

@ -32,10 +32,6 @@ open class BrowserInteractor(
browserToolbarController.handleToolbarItemInteraction(item)
}
override fun onBrowserMenuDismissed(lowPrioHighlightItems: List<ToolbarMenu.Item>) {
browserToolbarController.handleBrowserMenuDismissed(lowPrioHighlightItems)
}
override fun onScrolled(offset: Int) {
browserToolbarController.handleScroll(offset)
}

View File

@ -53,7 +53,6 @@ interface BrowserToolbarController {
fun handleToolbarClick()
fun handleTabCounterClick()
fun handleTabCounterItemInteraction(item: TabCounterMenuItem)
fun handleBrowserMenuDismissed(lowPrioHighlightItems: List<ToolbarMenu.Item>)
fun handleReaderModePressed(enabled: Boolean)
}
@ -87,14 +86,11 @@ class DefaultBrowserToolbarController(
internal var ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
override fun handleToolbarPaste(text: String) {
browserAnimator.captureEngineViewAndDrawStatically {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
sessionId = currentSession?.id,
pastedText = text
)
navController.nav(R.id.browserFragment, directions, getToolbarNavOptions(activity))
}
val directions = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
sessionId = currentSession?.id,
pastedText = text
)
navController.nav(R.id.browserFragment, directions, getToolbarNavOptions(activity))
}
override fun handleToolbarPasteAndGo(text: String) {
@ -113,13 +109,11 @@ class DefaultBrowserToolbarController(
Event.SearchBarTapped(Event.SearchBarTapped.Source.BROWSER)
)
browserAnimator.captureEngineViewAndDrawStatically {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
currentSession?.id
)
navController.nav(R.id.browserFragment, directions, getToolbarNavOptions(activity))
}
}
override fun handleTabCounterClick() {
@ -158,16 +152,6 @@ class DefaultBrowserToolbarController(
}
}
override fun handleBrowserMenuDismissed(lowPrioHighlightItems: List<ToolbarMenu.Item>) {
val settings = activity.settings()
lowPrioHighlightItems.forEach {
when (it) {
ToolbarMenu.Item.AddToHomeScreen -> settings.installPwaOpened = true
ToolbarMenu.Item.OpenInApp -> settings.openInAppOpened = true
}
}
}
override fun handleScroll(offset: Int) {
engineView.setVerticalClipping(offset)
}

View File

@ -50,7 +50,6 @@ interface BrowserToolbarViewInteractor {
fun onBrowserToolbarMenuItemTapped(item: ToolbarMenu.Item)
fun onTabCounterClicked()
fun onTabCounterMenuItemTapped(item: TabCounterMenuItem)
fun onBrowserMenuDismissed(lowPrioHighlightItems: List<ToolbarMenu.Item>)
fun onScrolled(offset: Int)
fun onReaderModePressed(enabled: Boolean)
}
@ -229,7 +228,6 @@ class BrowserToolbarView(
bookmarksStorage = bookmarkStorage
)
view.display.setMenuDismissAction {
interactor.onBrowserMenuDismissed(menuToolbar.getLowPrioHighlightItems())
view.invalidateActions()
}
}

View File

@ -137,17 +137,6 @@ class DefaultToolbarMenu(
BrowserMenuItemToolbar(listOf(bookmark, share, forward, refresh))
}
internal fun getLowPrioHighlightItems(): List<ToolbarMenu.Item> {
val lowPrioHighlightItems: MutableList<ToolbarMenu.Item> = mutableListOf()
if (canInstall() && installToHomescreen.isHighlighted()) {
lowPrioHighlightItems.add(ToolbarMenu.Item.InstallToHomeScreen)
}
if (shouldShowOpenInApp() && openInApp.isHighlighted()) {
lowPrioHighlightItems.add(ToolbarMenu.Item.OpenInApp)
}
return lowPrioHighlightItems
}
// Predicates that need to be repeatedly called as the session changes
private fun canAddToHomescreen(): Boolean =
session != null && context.components.useCases.webAppUseCases.isPinningSupported() &&

View File

@ -16,25 +16,20 @@ import kotlinx.android.synthetic.main.mozac_ui_tabcounter_layout.view.*
import org.mozilla.fenix.R
import java.text.NumberFormat
open class TabCounter @JvmOverloads constructor(
class TabCounter @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : RelativeLayout(context, attrs, defStyle) {
private val animationSet: AnimatorSet
private var count: Int = 0
init {
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.mozac_ui_tabcounter_layout, this)
counter_text.text = DEFAULT_TABS_COUNTER_TEXT
val shiftThreeDp = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, TWO_DIGIT_PADDING, context.resources.displayMetrics
).toInt()
counter_text.setPadding(0, shiftThreeDp, shiftThreeDp, 0)
updateContentDescription(0)
// This is needed because without this counter box will be empty.
setCount(INTERNAL_COUNT)
animationSet = createAnimatorSet()
}
@ -48,28 +43,20 @@ open class TabCounter @JvmOverloads constructor(
}
fun setCountWithAnimation(count: Int) {
updateContentDescription(count)
// Don't animate from initial state.
if (this.count == 0) {
setCount(count)
return
// No need to animate on these cases.
when {
count == INTERNAL_COUNT -> return // There isn't any tab added or removed.
INTERNAL_COUNT == 0 -> {
setCount(count)
return
} // Initial state.
count > MAX_VISIBLE_TABS && INTERNAL_COUNT > MAX_VISIBLE_TABS -> {
INTERNAL_COUNT = count
updateContentDescription(count)
return
} // There are still over MAX_VISIBLE_TABS tabs open.
}
if (this.count == count) {
return
}
// Don't animate if there are still over MAX_VISIBLE_TABS tabs open.
if (this.count > MAX_VISIBLE_TABS && count > MAX_VISIBLE_TABS) {
this.count = count
return
}
adjustTextSize(count)
counter_text.text = formatForDisplay(count)
this.count = count
setCount(count)
// Cancel previous animations if necessary.
if (animationSet.isRunning) {
@ -82,9 +69,8 @@ open class TabCounter @JvmOverloads constructor(
fun setCount(count: Int) {
updateContentDescription(count)
adjustTextSize(count)
counter_text.text = formatForDisplay(count)
this.count = count
INTERNAL_COUNT = count
}
private fun createAnimatorSet(): AnimatorSet {
@ -198,6 +184,7 @@ open class TabCounter @JvmOverloads constructor(
private fun formatForDisplay(count: Int): String {
return if (count > MAX_VISIBLE_TABS) {
counter_text.setPadding(0, 0, 0, INFINITE_CHAR_PADDING_BOTTOM)
SO_MANY_TABS_OPEN
} else NumberFormat.getInstance().format(count.toLong())
}
@ -217,14 +204,16 @@ open class TabCounter @JvmOverloads constructor(
}
companion object {
internal var INTERNAL_COUNT = 0
internal const val MAX_VISIBLE_TABS = 99
internal const val SO_MANY_TABS_OPEN = ""
internal const val DEFAULT_TABS_COUNTER_TEXT = ":)"
internal const val INFINITE_CHAR_PADDING_BOTTOM = 6
internal const val ONE_DIGIT_SIZE_RATIO = 0.5f
internal const val TWO_DIGITS_SIZE_RATIO = 0.4f
internal const val TWO_DIGIT_PADDING = 3F
internal const val TWO_DIGITS_TAB_COUNT_THRESHOLD = 10
// createBoxAnimatorSet

View File

@ -5,7 +5,7 @@
package org.mozilla.fenix.customtabs
import android.app.Activity
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.content.res.AppCompatResources.getDrawable
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.display.DisplayToolbar
@ -30,16 +30,13 @@ class CustomTabsIntegration(
// Remove toolbar shadow
toolbar.elevation = 0f
val uncoloredEtpShield = AppCompatResources.getDrawable(
activity,
R.drawable.ic_tracking_protection_enabled
)!!
val uncoloredEtpShield = getDrawable(activity, R.drawable.ic_tracking_protection_enabled)!!
toolbar.display.icons = toolbar.display.icons.copy(
// Custom private tab backgrounds have bad contrast against the colored shield
trackingProtectionTrackersBlocked = uncoloredEtpShield,
trackingProtectionNothingBlocked = uncoloredEtpShield,
trackingProtectionException = AppCompatResources.getDrawable(
trackingProtectionException = getDrawable(
activity,
R.drawable.ic_tracking_protection_disabled
)!!
@ -70,10 +67,7 @@ class CustomTabsIntegration(
)
}
toolbar.background = AppCompatResources.getDrawable(
activity,
R.drawable.toolbar_background
)
toolbar.background = getDrawable(activity, R.drawable.toolbar_background_private)
}
}

View File

@ -6,12 +6,10 @@ package org.mozilla.fenix.customtabs
import mozilla.components.concept.engine.Engine
import mozilla.components.feature.customtabs.AbstractCustomTabsService
import org.mozilla.fenix.BuildConfig.DIGITAL_ASSET_LINKS_TOKEN
import org.mozilla.fenix.ext.components
class CustomTabsService : AbstractCustomTabsService() {
override val engine: Engine by lazy { applicationContext.components.core.engine }
override val customTabsServiceStore by lazy { applicationContext.components.core.customTabsStore }
override val httpClient by lazy { applicationContext.components.core.client }
override val apiKey: String? = DIGITAL_ASSET_LINKS_TOKEN
override val engine: Engine by lazy { components.core.engine }
override val customTabsServiceStore by lazy { components.core.customTabsStore }
override val relationChecker by lazy { components.core.relationChecker }
}

View File

@ -46,6 +46,7 @@ import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.fragment_home.view.*
import kotlinx.android.synthetic.main.no_collections_message.view.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -231,6 +232,11 @@ class HomeFragment : Fragment() {
}
view.tab_button.setCountWithAnimation(tabCount)
view.add_tabs_to_collections_button?.visibility = if (tabCount > 0) {
View.VISIBLE
} else {
View.GONE
}
}
return view

View File

@ -17,6 +17,7 @@ import mozilla.components.service.fxa.sharing.ShareableAccount
import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
class OnboardingAutomaticSignInViewHolder(view: View) : RecyclerView.ViewHolder(view) {
@ -26,6 +27,8 @@ class OnboardingAutomaticSignInViewHolder(view: View) : RecyclerView.ViewHolder(
init {
view.turn_on_sync_button.setOnClickListener {
it.context.components.analytics.metrics.track(Event.OnboardingAutoSignIn)
it.turn_on_sync_button.text = it.context.getString(
R.string.onboarding_firefox_account_signing_in
)

View File

@ -8,6 +8,8 @@ import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.onboarding_finish.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.sessioncontrol.OnboardingInteractor
class OnboardingFinishViewHolder(
@ -18,6 +20,7 @@ class OnboardingFinishViewHolder(
init {
view.finish_button.setOnClickListener {
interactor.onStartBrowsingClicked()
it.context.components.analytics.metrics.track(Event.OnboardingFinish)
}
}

View File

@ -12,6 +12,8 @@ import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.onboarding_manual_signin.view.*
import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.HomeFragmentDirections
class OnboardingManualSignInViewHolder(view: View) : RecyclerView.ViewHolder(view) {
@ -20,6 +22,8 @@ class OnboardingManualSignInViewHolder(view: View) : RecyclerView.ViewHolder(vie
init {
view.turn_on_sync_button.setOnClickListener {
it.context.components.analytics.metrics.track(Event.OnboardingManualSignIn)
val directions = HomeFragmentDirections.actionGlobalTurnOnSync()
Navigation.findNavController(view).navigate(directions)
}

View File

@ -8,6 +8,8 @@ import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.onboarding_privacy_notice.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.sessioncontrol.OnboardingInteractor
class OnboardingPrivacyNoticeViewHolder(
@ -22,6 +24,7 @@ class OnboardingPrivacyNoticeViewHolder(
view.description_text.text = view.context.getString(R.string.onboarding_privacy_notice_description, appName)
view.read_button.setOnClickListener {
it.context.components.analytics.metrics.track(Event.OnboardingPrivacyNotice)
interactor.onReadPrivacyNoticeClicked()
}
}

View File

@ -16,6 +16,8 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.onboarding_private_browsing.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.setBounds
import org.mozilla.fenix.home.sessioncontrol.OnboardingInteractor
@ -47,6 +49,7 @@ class OnboardingPrivateBrowsingViewHolder(
view.description_text_once.text = text
view.description_text_once.contentDescription = String.format(text.toString(), view.header_text.text)
view.open_settings_button.setOnClickListener {
it.context.components.analytics.metrics.track(Event.OnboardingPrivateBrowsing)
interactor.onOpenSettingsClicked()
}
}

View File

@ -17,6 +17,7 @@ import kotlinx.android.synthetic.main.onboarding_theme_picker.view.theme_light_i
import kotlinx.android.synthetic.main.onboarding_theme_picker.view.theme_light_radio_button
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event.OnboardingThemePicker.Theme
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.onboarding.OnboardingRadioButton
@ -46,10 +47,12 @@ class OnboardingThemePickerViewHolder(view: View) : RecyclerView.ViewHolder(view
radioFollowDeviceTheme.addToRadioGroup(radioLightTheme)
view.theme_dark_image.setOnClickListener {
it.context.components.analytics.metrics.track(Event.OnboardingThemePicker(Theme.DARK))
radioDarkTheme.performClick()
}
view.theme_light_image.setOnClickListener {
it.context.components.analytics.metrics.track(Event.OnboardingThemePicker(Theme.LIGHT))
radioLightTheme.performClick()
}
@ -58,23 +61,26 @@ class OnboardingThemePickerViewHolder(view: View) : RecyclerView.ViewHolder(view
view.clickable_region_automatic.contentDescription = "$automaticTitle $automaticSummary"
view.clickable_region_automatic.setOnClickListener {
it.context.components.analytics.metrics
.track(Event.OnboardingThemePicker(Theme.FOLLOW_DEVICE))
radioFollowDeviceTheme.performClick()
}
radioLightTheme.onClickListener {
view.context.components.analytics.metrics
.track(Event.OnboardingThemePicker(Theme.LIGHT))
setNewTheme(AppCompatDelegate.MODE_NIGHT_NO)
}
radioDarkTheme.onClickListener {
view.context.components.analytics.metrics.track(
Event.DarkThemeSelected(
Event.DarkThemeSelected.Source.ONBOARDING
)
)
view.context.components.analytics.metrics
.track(Event.OnboardingThemePicker(Theme.DARK))
setNewTheme(AppCompatDelegate.MODE_NIGHT_YES)
}
radioFollowDeviceTheme.onClickListener {
view.context.components.analytics.metrics
.track(Event.OnboardingThemePicker(Theme.FOLLOW_DEVICE))
if (SDK_INT >= Build.VERSION_CODES.P) {
setNewTheme(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
} else {

View File

@ -11,7 +11,10 @@ import kotlinx.android.synthetic.main.onboarding_toolbar_position_picker.view.to
import kotlinx.android.synthetic.main.onboarding_toolbar_position_picker.view.toolbar_top_image
import kotlinx.android.synthetic.main.onboarding_toolbar_position_picker.view.toolbar_top_radio_button
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event.OnboardingToolbarPosition.Position
import org.mozilla.fenix.ext.asActivity
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.onboarding.OnboardingRadioButton
@ -36,18 +39,28 @@ class OnboardingToolbarPositionPickerViewHolder(view: View) : RecyclerView.ViewH
radio.updateRadioValue(true)
radioBottomToolbar.onClickListener {
itemView.context.components.analytics.metrics
.track(Event.OnboardingToolbarPosition(Position.BOTTOM))
itemView.context.asActivity()?.recreate()
}
view.toolbar_bottom_image.setOnClickListener {
itemView.context.components.analytics.metrics
.track(Event.OnboardingToolbarPosition(Position.BOTTOM))
radioBottomToolbar.performClick()
}
radioTopToolbar.onClickListener {
itemView.context.components.analytics.metrics
.track(Event.OnboardingToolbarPosition(Position.TOP))
itemView.context.asActivity()?.recreate()
}
view.toolbar_top_image.setOnClickListener {
itemView.context.components.analytics.metrics
.track(Event.OnboardingToolbarPosition(Position.TOP))
radioTopToolbar.performClick()
}
}

View File

@ -9,6 +9,8 @@ import androidx.appcompat.widget.SwitchCompat
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.onboarding_tracking_protection.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event.OnboardingTrackingProtection.Setting
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.onboarding.OnboardingRadioButton
@ -56,11 +58,15 @@ class OnboardingTrackingProtectionViewHolder(view: View) : RecyclerView.ViewHold
standardTrackingProtection.onClickListener {
updateTrackingProtectionPolicy()
view.context.components.analytics.metrics
.track(Event.OnboardingTrackingProtection(Setting.STANDARD))
}
view.clickable_region_standard.apply {
setOnClickListener {
standardTrackingProtection.performClick()
view.context.components.analytics.metrics
.track(Event.OnboardingTrackingProtection(Setting.STANDARD))
}
val standardTitle = view.context.getString(
R.string.onboarding_tracking_protection_standard_button_2
@ -73,11 +79,15 @@ class OnboardingTrackingProtectionViewHolder(view: View) : RecyclerView.ViewHold
strictTrackingProtection.onClickListener {
updateTrackingProtectionPolicy()
view.context.components.analytics.metrics
.track(Event.OnboardingTrackingProtection(Setting.STRICT))
}
view.clickable_region_strict.apply {
setOnClickListener {
strictTrackingProtection.performClick()
view.context.components.analytics.metrics
.track(Event.OnboardingTrackingProtection(Setting.STRICT))
}
val strictTitle =
view.context.getString(R.string.onboarding_tracking_protection_strict_option)

View File

@ -10,6 +10,8 @@ import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.onboarding_whats_new.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.sessioncontrol.OnboardingInteractor
class OnboardingWhatsNewViewHolder(
@ -31,6 +33,7 @@ class OnboardingWhatsNewViewHolder(
view.get_answers.text = textWithLink
view.get_answers.setOnClickListener {
interactor.onWhatsNewGetAnswersClicked()
view.context.components.analytics.metrics.track(Event.OnboardingWhatsNew)
}
}

View File

@ -4,8 +4,11 @@
package org.mozilla.fenix.home.sessioncontrol.viewholders.topsites
import android.annotation.SuppressLint
import android.content.Context
import android.view.MotionEvent
import android.view.View
import android.widget.PopupWindow
import kotlinx.android.synthetic.main.top_site_item.*
import kotlinx.android.synthetic.main.top_site_item.view.*
import mozilla.components.browser.menu.BrowserMenuBuilder
@ -40,7 +43,10 @@ class TopSiteItemViewHolder(
}
top_site_item.setOnLongClickListener() {
topSiteMenu.menuBuilder.build(view.context).show(anchor = it.top_site_title)
val menu = topSiteMenu.menuBuilder.build(view.context).show(anchor = it.top_site_title)
it.setOnTouchListener @SuppressLint("ClickableViewAccessibility") { v, event ->
onTouchEvent(v, event, menu)
}
return@setOnLongClickListener true
}
}
@ -58,6 +64,17 @@ class TopSiteItemViewHolder(
}
}
private fun onTouchEvent(
v: View,
event: MotionEvent,
menu: PopupWindow
): Boolean {
if (event.action == MotionEvent.ACTION_CANCEL) {
menu.dismiss()
}
return v.onTouchEvent(event)
}
companion object {
const val LAYOUT_ID = R.layout.top_site_item
}

View File

@ -11,13 +11,19 @@ import android.content.res.Resources
import androidx.core.content.getSystemService
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.service.fxa.sync.SyncReason
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.bookmarkStorage
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
/**
@ -26,16 +32,20 @@ import org.mozilla.fenix.ext.nav
*/
@SuppressWarnings("TooManyFunctions")
interface BookmarkController {
fun handleBookmarkChanged(item: BookmarkNode)
fun handleBookmarkTapped(item: BookmarkNode)
fun handleBookmarkExpand(folder: BookmarkNode)
fun handleSelectionModeSwitch()
fun handleBookmarkEdit(node: BookmarkNode)
fun handleBookmarkSelected(node: BookmarkNode)
fun handleBookmarkDeselected(node: BookmarkNode)
fun handleAllBookmarksDeselected()
fun handleCopyUrl(item: BookmarkNode)
fun handleBookmarkSharing(item: BookmarkNode)
fun handleOpeningBookmark(item: BookmarkNode, mode: BrowsingMode)
fun handleBookmarkDeletion(nodes: Set<BookmarkNode>, eventType: Event)
fun handleBookmarkFolderDeletion(node: BookmarkNode)
fun handleRequestSync()
fun handleBackPressed()
}
@ -43,6 +53,10 @@ interface BookmarkController {
class DefaultBookmarkController(
private val context: Context,
private val navController: NavController,
private val scope: CoroutineScope,
private val store: BookmarkFragmentStore,
private val sharedViewModel: BookmarksSharedViewModel,
private val loadBookmarkNode: suspend (String) -> BookmarkNode?,
private val showSnackbar: (String) -> Unit,
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit,
private val deleteBookmarkFolder: (BookmarkNode) -> Unit,
@ -52,12 +66,23 @@ class DefaultBookmarkController(
private val activity: HomeActivity = context as HomeActivity
private val resources: Resources = context.resources
override fun handleBookmarkChanged(item: BookmarkNode) {
sharedViewModel.selectedFolder = item
store.dispatch(BookmarkFragmentAction.Change(item))
}
override fun handleBookmarkTapped(item: BookmarkNode) {
openInNewTab(item.url!!, true, BrowserDirection.FromBookmarks, activity.browsingModeManager.mode)
}
override fun handleBookmarkExpand(folder: BookmarkNode) {
navigate(BookmarkFragmentDirections.actionBookmarkFragmentSelf(folder.guid))
handleAllBookmarksDeselected()
invokePendingDeletion.invoke()
scope.launch {
val node = loadBookmarkNode.invoke(folder.guid) ?: return@launch
sharedViewModel.selectedFolder = node
store.dispatch(BookmarkFragmentAction.Change(node))
}
}
override fun handleSelectionModeSwitch() {
@ -69,7 +94,23 @@ class DefaultBookmarkController(
}
override fun handleBookmarkSelected(node: BookmarkNode) {
showSnackbar(resources.getString(R.string.bookmark_cannot_edit_root))
if (store.state.mode is BookmarkFragmentState.Mode.Syncing) {
return
}
if (node.inRoots()) {
showSnackbar(resources.getString(R.string.bookmark_cannot_edit_root))
} else {
store.dispatch(BookmarkFragmentAction.Select(node))
}
}
override fun handleBookmarkDeselected(node: BookmarkNode) {
store.dispatch(BookmarkFragmentAction.Deselect(node))
}
override fun handleAllBookmarksDeselected() {
store.dispatch(BookmarkFragmentAction.DeselectAll)
}
override fun handleCopyUrl(item: BookmarkNode) {
@ -98,9 +139,36 @@ class DefaultBookmarkController(
deleteBookmarkFolder(node)
}
override fun handleRequestSync() {
scope.launch {
store.dispatch(BookmarkFragmentAction.StartSync)
invokePendingDeletion()
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.User).await()
// The current bookmark node we are viewing may be made invalid after syncing so we
// check if the current node is valid and if it isn't we find the nearest valid ancestor
// and open it
val validAncestorGuid = store.state.guidBackstack.findLast { guid ->
context.bookmarkStorage.getBookmark(guid) != null
} ?: BookmarkRoot.Mobile.id
val node = context.bookmarkStorage.getBookmark(validAncestorGuid)!!
handleBookmarkExpand(node)
store.dispatch(BookmarkFragmentAction.FinishSync)
}
}
override fun handleBackPressed() {
invokePendingDeletion.invoke()
navController.popBackStack()
scope.launch {
val parentGuid = store.state.guidBackstack.findLast { guid ->
store.state.tree?.guid != guid && context.bookmarkStorage.getBookmark(guid) != null
}
if (parentGuid == null) {
navController.popBackStack()
} else {
val parent = context.bookmarkStorage.getBookmark(parentGuid)!!
handleBookmarkExpand(parent)
}
}
}
private fun openInNewTab(

View File

@ -13,18 +13,19 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.component_bookmark.view.*
import kotlinx.android.synthetic.main.fragment_bookmark.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.isActive
@ -59,7 +60,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
private lateinit var bookmarkStore: BookmarkFragmentStore
private lateinit var bookmarkView: BookmarkView
private var _bookmarkInteractor: BookmarkFragmentInteractor? = null
protected val bookmarkInteractor: BookmarkFragmentInteractor
private val bookmarkInteractor: BookmarkFragmentInteractor
get() = _bookmarkInteractor!!
private val sharedViewModel: BookmarksSharedViewModel by activityViewModels {
@ -67,7 +68,6 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
}
private val desktopFolders by lazy { DesktopFolders(requireContext(), showMobileRoot = false) }
lateinit var initialJob: Job
private var pendingBookmarkDeletionJob: (suspend () -> Unit)? = null
private var pendingBookmarksToDelete: MutableSet<BookmarkNode> = mutableSetOf()
@ -84,11 +84,13 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
}
_bookmarkInteractor = BookmarkFragmentInteractor(
bookmarkStore = bookmarkStore,
viewModel = sharedViewModel,
bookmarksController = DefaultBookmarkController(
context = requireContext(),
navController = findNavController(),
scope = viewLifecycleOwner.lifecycleScope,
store = bookmarkStore,
sharedViewModel = sharedViewModel,
loadBookmarkNode = ::loadBookmarkNode,
showSnackbar = ::showSnackBarWithText,
deleteBookmarkNodes = ::deleteMulti,
deleteBookmarkFolder = ::showRemoveFolderDialog,
@ -124,8 +126,16 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val accountManager = requireComponents.backgroundServices.accountManager
consumeFrom(bookmarkStore) {
bookmarkView.update(it)
// Only display the sign-in prompt if we're inside of the virtual "Desktop Bookmarks" node.
// Don't want to pester user too much with it, and if there are lots of bookmarks present,
// it'll just get visually lost. Inside of the "Desktop Bookmarks" node, it'll nicely stand-out,
// since there are always only three other items in there. It's also the right place contextually.
bookmarkView.view.bookmark_folders_sign_in.isVisible =
it.tree?.guid == BookmarkRoot.Root.id && accountManager.authenticatedAccount() == null
}
}
@ -138,36 +148,24 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
super.onResume()
(activity as HomeActivity).getSupportActionBarAndInflateIfNecessary().show()
val currentGuid = BookmarkFragmentArgs.fromBundle(requireArguments()).currentRoot.ifEmpty {
BookmarkRoot.Mobile.id
}
// Only display the sign-in prompt if we're inside of the virtual "Desktop Bookmarks" node.
// Don't want to pester user too much with it, and if there are lots of bookmarks present,
// it'll just get visually lost. Inside of the "Desktop Bookmarks" node, it'll nicely stand-out,
// since there are always only three other items in there. It's also the right place contextually.
if (currentGuid == BookmarkRoot.Root.id &&
requireComponents.backgroundServices.accountManager.authenticatedAccount() == null
) {
bookmarkView.view.bookmark_folders_sign_in.visibility = View.VISIBLE
} else {
bookmarkView.view.bookmark_folders_sign_in.visibility = View.GONE
}
initialJob = loadInitialBookmarkFolder(currentGuid)
// Reload bookmarks when returning to this fragment in case they have been edited
val args by navArgs<BookmarkFragmentArgs>()
val currentGuid = bookmarkStore.state.tree?.guid
?: if (args.currentRoot.isNotEmpty()) {
args.currentRoot
} else {
BookmarkRoot.Mobile.id
}
loadInitialBookmarkFolder(currentGuid)
}
private fun loadInitialBookmarkFolder(currentGuid: String): Job {
return viewLifecycleOwner.lifecycleScope.launch(Main) {
val currentRoot = withContext(IO) {
requireContext().bookmarkStorage
.getTree(currentGuid)
?.let { desktopFolders.withOptionalDesktopFolders(it) }!!
}
private fun loadInitialBookmarkFolder(currentGuid: String) {
viewLifecycleOwner.lifecycleScope.launch(Main) {
val currentRoot = loadBookmarkNode(currentGuid)
if (isActive) {
if (isActive && currentRoot != null) {
bookmarkInteractor.onBookmarksChanged(currentRoot)
sharedViewModel.selectedFolder = currentRoot
}
}
}
@ -249,14 +247,18 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
return bookmarkView.onBackPressed()
}
private suspend fun loadBookmarkNode(guid: String): BookmarkNode? = withContext(IO) {
requireContext().bookmarkStorage
.getTree(guid, false)
?.let { desktopFolders.withOptionalDesktopFolders(it) }
}
private suspend fun refreshBookmarks() {
// The bookmark tree in our 'state' can be null - meaning, no bookmark tree has been selected.
// If that's the case, we don't know what node to refresh, and so we bail out.
// See https://github.com/mozilla-mobile/fenix/issues/4671
val currentGuid = bookmarkStore.state.tree?.guid ?: return
context?.bookmarkStorage
?.getTree(currentGuid, false)
?.let { desktopFolders.withOptionalDesktopFolders(it) }
loadBookmarkNode(currentGuid)
?.let { node ->
val rootNode = node - pendingBookmarksToDelete
bookmarkInteractor.onBookmarksChanged(rootNode)

View File

@ -22,14 +22,12 @@ import org.mozilla.fenix.utils.Do
*/
@SuppressWarnings("TooManyFunctions")
class BookmarkFragmentInteractor(
private val bookmarkStore: BookmarkFragmentStore,
private val viewModel: BookmarksSharedViewModel,
private val bookmarksController: BookmarkController,
private val metrics: MetricController
) : BookmarkViewInteractor {
override fun onBookmarksChanged(node: BookmarkNode) {
bookmarkStore.dispatch(BookmarkFragmentAction.Change(node))
bookmarksController.handleBookmarkChanged(node)
}
override fun onSelectionModeSwitch(mode: BookmarkFragmentState.Mode) {
@ -41,7 +39,7 @@ class BookmarkFragmentInteractor(
}
override fun onAllBookmarksDeselected() {
bookmarkStore.dispatch(BookmarkFragmentAction.DeselectAll)
bookmarksController.handleAllBookmarksDeselected()
}
/**
@ -112,13 +110,14 @@ class BookmarkFragmentInteractor(
}
override fun select(item: BookmarkNode) {
when (item.inRoots()) {
true -> bookmarksController.handleBookmarkSelected(item)
false -> bookmarkStore.dispatch(BookmarkFragmentAction.Select(item))
}
bookmarksController.handleBookmarkSelected(item)
}
override fun deselect(item: BookmarkNode) {
bookmarkStore.dispatch(BookmarkFragmentAction.Deselect(item))
bookmarksController.handleBookmarkDeselected(item)
}
override fun onRequestSync() {
bookmarksController.handleRequestSync()
}
}

View File

@ -20,10 +20,14 @@ class BookmarkFragmentStore(
* The complete state of the bookmarks tree and multi-selection mode
* @property tree The current tree of bookmarks, if one is loaded
* @property mode The current bookmark multi-selection mode
* @property guidBackstack A set of guids for bookmark nodes we have visited. Used to traverse back
* up the tree after a sync.
* @property isLoading true if bookmarks are still being loaded from disk
*/
data class BookmarkFragmentState(
val tree: BookmarkNode?,
val mode: Mode = Mode.Normal(),
val guidBackstack: List<String> = emptyList(),
val isLoading: Boolean = true
) : State {
sealed class Mode {
@ -31,6 +35,7 @@ data class BookmarkFragmentState(
data class Normal(val showMenu: Boolean = true) : Mode()
data class Selecting(override val selectedItems: Set<BookmarkNode>) : Mode()
object Syncing : Mode()
}
}
@ -42,6 +47,8 @@ sealed class BookmarkFragmentAction : Action {
data class Select(val item: BookmarkNode) : BookmarkFragmentAction()
data class Deselect(val item: BookmarkNode) : BookmarkFragmentAction()
object DeselectAll : BookmarkFragmentAction()
object StartSync : BookmarkFragmentAction()
object FinishSync : BookmarkFragmentAction()
}
/**
@ -56,16 +63,26 @@ private fun bookmarkFragmentStateReducer(
): BookmarkFragmentState {
return when (action) {
is BookmarkFragmentAction.Change -> {
// If we change to a node we have already visited, we pop the backstack until the node
// is the last item. If we haven't visited the node yet, we just add it to the end of the
// backstack
val backstack = state.guidBackstack.takeWhile { guid ->
guid != action.tree.guid
} + action.tree.guid
val items = state.mode.selectedItems.filter { it in action.tree }
state.copy(
tree = action.tree,
mode = if (BookmarkRoot.Root.id == action.tree.guid) {
BookmarkFragmentState.Mode.Normal(false)
} else if (items.isEmpty()) {
BookmarkFragmentState.Mode.Normal()
} else {
BookmarkFragmentState.Mode.Selecting(items.toSet())
mode = when {
state.mode is BookmarkFragmentState.Mode.Syncing -> {
BookmarkFragmentState.Mode.Syncing
}
items.isEmpty() -> {
BookmarkFragmentState.Mode.Normal(shouldShowMenu(action.tree.guid))
}
else -> BookmarkFragmentState.Mode.Selecting(items.toSet())
},
guidBackstack = backstack,
isLoading = false
)
}
@ -81,11 +98,30 @@ private fun bookmarkFragmentStateReducer(
}
)
}
BookmarkFragmentAction.DeselectAll ->
state.copy(mode = BookmarkFragmentState.Mode.Normal())
is BookmarkFragmentAction.DeselectAll ->
state.copy(
mode = if (state.mode is BookmarkFragmentState.Mode.Syncing) {
BookmarkFragmentState.Mode.Syncing
} else {
BookmarkFragmentState.Mode.Normal()
}
)
is BookmarkFragmentAction.StartSync ->
state.copy(
mode = BookmarkFragmentState.Mode.Syncing
)
is BookmarkFragmentAction.FinishSync ->
state.copy(
mode = BookmarkFragmentState.Mode.Normal(
showMenu = shouldShowMenu(state.tree?.guid)
)
)
}
}
private fun shouldShowMenu(currentGuid: String?): Boolean =
BookmarkRoot.Root.id != currentGuid
operator fun BookmarkNode.contains(item: BookmarkNode): Boolean {
return children?.contains(item) ?: false
}

View File

@ -93,6 +93,12 @@ interface BookmarkViewInteractor : SelectionInteractor<BookmarkNode> {
*
*/
fun onBackPressed()
/**
* Handles user requested sync of bookmarks.
*
*/
fun onRequestSync()
}
class BookmarkView(
@ -106,7 +112,6 @@ class BookmarkView(
private var mode: BookmarkFragmentState.Mode = BookmarkFragmentState.Mode.Normal()
private var tree: BookmarkNode? = null
private var canGoBack = false
private val bookmarkAdapter: BookmarkAdapter
@ -118,14 +123,18 @@ class BookmarkView(
view.bookmark_folders_sign_in.setOnClickListener {
navController.navigate(NavGraphDirections.actionGlobalTurnOnSync())
}
view.swipe_refresh.setOnRefreshListener {
interactor.onRequestSync()
}
}
fun update(state: BookmarkFragmentState) {
canGoBack = BookmarkRoot.Root.matches(state.tree)
tree = state.tree
if (state.mode != mode) {
mode = state.mode
interactor.onSelectionModeSwitch(mode)
if (mode is BookmarkFragmentState.Mode.Normal || mode is BookmarkFragmentState.Mode.Selecting) {
interactor.onSelectionModeSwitch(mode)
}
}
bookmarkAdapter.updateData(state.tree, mode)
@ -151,19 +160,21 @@ class BookmarkView(
}
}
view.bookmarks_progress_bar.isVisible = state.isLoading
view.swipe_refresh.isEnabled =
state.mode is BookmarkFragmentState.Mode.Normal || state.mode is BookmarkFragmentState.Mode.Syncing
view.swipe_refresh.isRefreshing = state.mode is BookmarkFragmentState.Mode.Syncing
}
override fun onBackPressed(): Boolean {
return when {
mode is BookmarkFragmentState.Mode.Selecting -> {
return when (mode) {
is BookmarkFragmentState.Mode.Selecting -> {
interactor.onAllBookmarksDeselected()
true
}
canGoBack -> {
else -> {
interactor.onBackPressed()
true
}
else -> false
}
}

View File

@ -7,9 +7,6 @@ package org.mozilla.fenix.search
import android.content.Intent
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
import mozilla.components.support.ktx.kotlin.isUrl
@ -51,7 +48,6 @@ class DefaultSearchController(
private val activity: HomeActivity,
private val store: SearchFragmentStore,
private val navController: NavController,
private val viewLifecycleScope: CoroutineScope,
private val clearToolbarFocus: () -> Unit
) : SearchController {
@ -101,13 +97,7 @@ class DefaultSearchController(
}
override fun handleEditingCancelled() {
viewLifecycleScope.launch {
clearToolbarFocus()
// Delay a short amount so the keyboard begins animating away. This makes exit animation
// much smoother instead of having two separate parts (keyboard hides THEN animation)
delay(KEYBOARD_ANIMATION_DELAY)
navController.popBackStack()
}
clearToolbarFocus()
}
override fun handleTextChanged(text: String) {
@ -199,8 +189,4 @@ class DefaultSearchController(
handleExistingSessionSelected(session)
}
}
companion object {
internal const val KEYBOARD_ANIMATION_DELAY = 5L
}
}

View File

@ -24,7 +24,6 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_search.*
@ -120,7 +119,6 @@ class SearchFragment : Fragment(), UserInteractionHandler {
activity = activity,
store = searchStore,
navController = findNavController(),
viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
clearToolbarFocus = ::clearToolbarFocus
)
@ -182,6 +180,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
}
private fun clearToolbarFocus() {
toolbarView.view.hideKeyboard()
toolbarView.view.clearFocus()
}
@ -347,14 +346,14 @@ class SearchFragment : Fragment(), UserInteractionHandler {
}
override fun onBackPressed(): Boolean {
// Note: Actual navigation happens in `handleEditingCancelled` in SearchController
return when {
qrFeature.onBackPressed() -> {
toolbarView.view.edit.focus()
view?.search_scan_button?.isChecked = false
toolbarView.view.requestFocus()
true
}
else -> true
else -> false
}
}
@ -426,7 +425,6 @@ class SearchFragment : Fragment(), UserInteractionHandler {
}
companion object {
private const val SHARED_TRANSITION_MS = 250L
private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1
}
}

View File

@ -59,7 +59,6 @@ class ToolbarView(
) {
private var isInitialized = false
private var hasBeenCanceled = false
init {
view.apply {
@ -96,19 +95,18 @@ class ToolbarView(
)
edit.setUrlBackground(
AppCompatResources.getDrawable(context, R.drawable.search_url_background))
AppCompatResources.getDrawable(context, R.drawable.search_url_background)
)
private = isPrivate
setOnEditListener(object : mozilla.components.concept.toolbar.Toolbar.OnEditListener {
override fun onCancelEditing(): Boolean {
// For some reason, this can be triggered twice on one back press. This only leads to
// navigateUp, so let's make sure we only call it once
if (!hasBeenCanceled) interactor.onEditingCanceled()
hasBeenCanceled = true
interactor.onEditingCanceled()
// We need to return false to not show display mode
return false
}
override fun onTextChanged(text: String) {
url = text
this@ToolbarView.interactor.onTextChanged(text)
@ -144,13 +142,15 @@ class ToolbarView(
isInitialized = true
}
val iconSize = context.resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
val iconSize =
context.resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
val scaledIcon = Bitmap.createScaledBitmap(
searchState.searchEngineSource.searchEngine.icon,
iconSize,
iconSize,
true)
true
)
val icon = BitmapDrawable(context.resources, scaledIcon)

View File

@ -20,25 +20,22 @@ import org.mozilla.fenix.ext.components
* indicating that a private tab is open.
*/
class NotificationSessionObserver(
private val context: Context,
private val applicationContext: Context,
private val notificationService: SessionNotificationService.Companion = SessionNotificationService
) {
private var scope: CoroutineScope? = null
private var started = false
@ExperimentalCoroutinesApi
fun start() {
scope = context.components.core.store.flowScoped { flow ->
scope = applicationContext.components.core.store.flowScoped { flow ->
flow.map { state -> state.privateTabs.isNotEmpty() }
.ifChanged()
.collect { hasPrivateTabs ->
if (hasPrivateTabs) {
notificationService.start(context)
started = true
} else if (started) {
notificationService.stop(context)
started = false
notificationService.start(applicationContext, isStartedFromPrivateShortcut)
} else if (SessionNotificationService.started) {
notificationService.stop(applicationContext)
}
}
}
@ -47,4 +44,8 @@ class NotificationSessionObserver(
fun stop() {
scope?.cancel()
}
companion object {
var isStartedFromPrivateShortcut = false
}
}

View File

@ -37,32 +37,41 @@ import org.mozilla.fenix.ext.sessionsOfType
*/
class SessionNotificationService : Service() {
private var isStartedFromPrivateShortcut: Boolean = false
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val action = intent.action ?: return Service.START_NOT_STICKY
val action = intent.action ?: return START_NOT_STICKY
when (action) {
ACTION_START -> {
isStartedFromPrivateShortcut = intent.getBooleanExtra(STARTED_FROM_PRIVATE_SHORTCUT, false)
createNotificationChannelIfNeeded()
startForeground(NOTIFICATION_ID, buildNotification())
}
ACTION_ERASE -> {
metrics.track(Event.PrivateBrowsingNotificationTapped)
components.core.sessionManager.removeAndCloseAllPrivateSessions()
if (!VisibilityLifecycleCallback.finishAndRemoveTaskIfInBackground(this)) {
startActivity(
Intent(this, HomeActivity::class.java).apply {
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
)
val homeScreenIntent = Intent(this, HomeActivity::class.java)
val intentFlags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
homeScreenIntent.apply {
setFlags(intentFlags)
putExtra(HomeActivity.PRIVATE_BROWSING_MODE, isStartedFromPrivateShortcut)
}
if (VisibilityLifecycleCallback.finishAndRemoveTaskIfInBackground(this)) {
// Set start mode to be in background (recents screen)
homeScreenIntent.apply {
putExtra(HomeActivity.START_IN_RECENTS_SCREEN, true)
}
}
startActivity(homeScreenIntent)
components.core.sessionManager.removeAndCloseAllPrivateSessions()
}
else -> throw IllegalStateException("Unknown intent: $intent")
}
return Service.START_NOT_STICKY
return START_NOT_STICKY
}
override fun onTaskRemoved(rootIntent: Intent) {
@ -125,13 +134,19 @@ class SessionNotificationService : Service() {
companion object {
private const val NOTIFICATION_ID = 83
private const val NOTIFICATION_CHANNEL_ID = "browsing-session"
private const val STARTED_FROM_PRIVATE_SHORTCUT = "STARTED_FROM_PRIVATE_SHORTCUT"
private const val ACTION_START = "start"
private const val ACTION_ERASE = "erase"
internal var started = false
internal fun start(context: Context) {
internal fun start(
context: Context,
startedFromPrivateShortcut: Boolean
) {
val intent = Intent(context, SessionNotificationService::class.java)
intent.action = ACTION_START
intent.putExtra(STARTED_FROM_PRIVATE_SHORTCUT, startedFromPrivateShortcut)
// From Focus #2901: The application is crashing due to the service not calling `startForeground`
// before it times out. This is a speculative fix to decrease the time between these two
@ -140,6 +155,8 @@ class SessionNotificationService : Service() {
ThreadUtils.postToMainThread(Runnable {
context.startService(intent)
})
started = true
}
internal fun stop(context: Context) {
@ -150,6 +167,8 @@ class SessionNotificationService : Service() {
ThreadUtils.postToMainThread(Runnable {
context.stopService(intent)
})
started = false
}
}
}

View File

@ -25,6 +25,13 @@ class VisibilityLifecycleCallback(private val activityManager: ActivityManager?)
*/
private var activitiesInStartedState: Int = 0
/**
* Finishes and removes the list of AppTasks only if the application is in the background.
* The application is considered to be in the background if it has at least 1 Activity in the
* started state
* @return True if application is in background (also finishes and removes all AppTasks),
* false otherwise
*/
private fun finishAndRemoveTaskIfInBackground(): Boolean {
if (activitiesInStartedState == 0) {
activityManager?.let {
@ -59,6 +66,9 @@ class VisibilityLifecycleCallback(private val activityManager: ActivityManager?)
/**
* If all activities of this app are in the background then finish and remove all tasks. After
* that the app won't show up in "recent apps" anymore.
*
* @return True if application is in background (and consequently, finishes and removes all tasks),
* false otherwise.
*/
internal fun finishAndRemoveTaskIfInBackground(context: Context): Boolean {
return (context.applicationContext as FenixApplication)

View File

@ -24,6 +24,7 @@ import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.TabData
import mozilla.components.feature.accounts.push.SendTabUseCases
import mozilla.components.feature.share.RecentAppsStorage
import mozilla.components.support.ktx.kotlin.isExtensionUrl
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
@ -172,7 +173,20 @@ class DefaultShareController(
@VisibleForTesting
fun getShareText() = shareData.joinToString("\n\n") { data ->
data.url.orEmpty()
val url = data.url.orEmpty()
if (url.isExtensionUrl()) {
// Sharing moz-extension:// URLs is not practical in general, as
// they will only work on the current device.
// We solve this for URLs from our reader extension as they contain
// the original URL as a query parameter. This is a workaround for
// now and needs a clean fix once we have a reader specific protocol
// e.g. ext+reader://
// https://github.com/mozilla-mobile/android-components/issues/2879
Uri.parse(url).getQueryParameter("url") ?: url
} else {
url
}
}
// Navigation between app fragments uses ShareTab as arguments. SendTabUseCases uses TabData.

View File

@ -33,7 +33,11 @@ class ShareTabsAdapter :
class ShareTabViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(item: ShareData) = with(itemView) {
context.components.core.icons.loadIntoView(itemView.share_tab_favicon, item.url.orEmpty())
val url = item.url
if (!url.isNullOrEmpty()) {
context.components.core.icons.loadIntoView(itemView.share_tab_favicon, url)
}
itemView.share_tab_title.text = item.title
itemView.share_tab_url.text = item.url
}

View File

@ -8,15 +8,13 @@ import android.content.Context
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat.getColor
import androidx.core.graphics.BlendModeColorFilterCompat.createBlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat.SRC_IN
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.account_share_list_item.view.*
import mozilla.components.concept.sync.DeviceType
import org.mozilla.fenix.R
import org.mozilla.fenix.utils.Do
import org.mozilla.fenix.share.ShareToAccountDevicesInteractor
import org.mozilla.fenix.share.listadapters.SyncShareOption
import org.mozilla.fenix.utils.Do
class AccountDeviceViewHolder(
itemView: View,
@ -52,7 +50,7 @@ class AccountDeviceViewHolder(
itemView.deviceIcon.apply {
setImageResource(drawableRes)
background.colorFilter = createBlendModeColorFilterCompat(getColor(context, colorRes), SRC_IN)
background.setTint(getColor(context, colorRes))
drawable.setTint(getColor(context, R.color.device_foreground))
}
itemView.isClickable = option != SyncShareOption.Offline

View File

@ -126,7 +126,6 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
view.context.components.core.store,
selectTabUseCase,
removeTabUseCase,
view.context.components.useCases.thumbnailUseCases,
{ it.content.private == isPrivate },
{ }
),

View File

@ -122,7 +122,7 @@ class TabTrayView(
}
if (!hasLoaded) {
hasLoaded = true
tray.layoutManager?.scrollToPosition(selectedBrowserTabIndex)
scrollToSelectedTab()
if (view.context.settings().accessibilityServicesEnabled) {
lifecycleScope.launch {
delay(SELECTION_DELAY.toLong())
@ -194,6 +194,7 @@ class TabTrayView(
filterTabs.invoke(filter)
updateState(view.context.components.core.store.state)
scrollToSelectedTab()
}
override fun onTabReselected(tab: TabLayout.Tab?) { /*noop*/ }
@ -225,16 +226,6 @@ class TabTrayView(
}
}
private fun toggleFabText(private: Boolean) {
if (private) {
fabView.new_tab_button.extend()
fabView.new_tab_button.contentDescription = view.context.resources.getString(R.string.add_private_tab)
} else {
fabView.new_tab_button.shrink()
fabView.new_tab_button.contentDescription = view.context.resources.getString(R.string.add_tab)
}
}
fun setTopOffset(landscape: Boolean) {
val topOffset = if (landscape) {
0
@ -249,6 +240,31 @@ class TabTrayView(
menu?.dismiss()
}
private fun toggleFabText(private: Boolean) {
if (private) {
fabView.new_tab_button.extend()
fabView.new_tab_button.contentDescription = view.context.resources.getString(R.string.add_private_tab)
} else {
fabView.new_tab_button.shrink()
fabView.new_tab_button.contentDescription = view.context.resources.getString(R.string.add_tab)
}
}
private fun scrollToSelectedTab() {
(view.tabsTray as? BrowserTabsTray)?.also { tray ->
val tabs = if (isPrivateModeSelected) {
view.context.components.core.store.state.privateTabs
} else {
view.context.components.core.store.state.normalTabs
}
val selectedBrowserTabIndex = tabs
.indexOfFirst { it.id == view.context.components.core.store.state.selectedTabId }
tray.layoutManager?.scrollToPosition(selectedBrowserTabIndex)
}
}
companion object {
private const val DEFAULT_TAB_ID = 0
private const val PRIVATE_TAB_ID = 1

View File

@ -185,15 +185,16 @@ class TabTrayViewHolder(
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info?.let {
val initialInfo = info.collectionItemInfo
info.collectionItemInfo = AccessibilityNodeInfo.CollectionItemInfo.obtain(
newIndex,
initialInfo.rowSpan,
initialInfo.columnIndex,
initialInfo.columnSpan,
false,
initialInfo.isSelected
)
info.collectionItemInfo = info.collectionItemInfo?.let { initialInfo ->
AccessibilityNodeInfo.CollectionItemInfo.obtain(
newIndex,
initialInfo.rowSpan,
initialInfo.columnIndex,
initialInfo.columnSpan,
false,
initialInfo.isSelected
)
}
}
}
})

View File

@ -10,6 +10,7 @@ import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.widget.ImageView
import androidx.core.view.isGone
@ -19,6 +20,8 @@ import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.*
import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.view.*
import mozilla.components.browser.session.Session
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.utils.Settings
@ -40,13 +43,23 @@ class TrackingProtectionOverlay(
private fun shouldShowTrackingProtectionOnboarding(session: Session) =
settings.shouldShowTrackingProtectionOnboarding &&
session.trackerBlockingEnabled &&
session.trackersBlocked.isNotEmpty()
session.trackerBlockingEnabled &&
session.trackersBlocked.isNotEmpty()
@Suppress("MagicNumber", "InflateParams")
private fun showTrackingProtectionOnboarding() {
if (!getToolbar().hasWindowFocus()) return
val trackingOnboardingDialog = Dialog(context)
val trackingOnboardingDialog = object : Dialog(context) {
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
context.components.analytics.metrics.track(Event.ContextualHintETPOutsideTap)
}
return super.onTouchEvent(event)
}
}
val layout = LayoutInflater.from(context)
.inflate(R.layout.tracking_protection_onboarding_popup, null)
val isBottomToolbar = Settings.getInstance(context).shouldUseBottomToolbar
@ -63,6 +76,7 @@ class TrackingProtectionOverlay(
val closeButton = layout.findViewById<ImageView>(R.id.close_onboarding)
closeButton.increaseTapArea(BUTTON_INCREASE_DPS)
closeButton.setOnClickListener {
context.components.analytics.metrics.track(Event.ContextualHintETPDismissed)
trackingOnboardingDialog.dismiss()
}
@ -101,10 +115,12 @@ class TrackingProtectionOverlay(
val etpShield =
getToolbar().findViewById<View>(R.id.mozac_browser_toolbar_tracking_protection_indicator)
trackingOnboardingDialog.message.setOnClickListener {
context.components.analytics.metrics.track(Event.ContextualHintETPInsideTap)
trackingOnboardingDialog.dismiss()
etpShield.performClick()
}
context.components.analytics.metrics.track(Event.ContextualHintETPDisplayed)
trackingOnboardingDialog.show()
settings.incrementTrackingProtectionOnboardingCount()
}

View File

@ -224,11 +224,6 @@ class Settings private constructor(
default = ""
)
var readerModeOpened by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_reader_mode_opened),
default = false
)
var openInAppOpened by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_open_in_app_opened),
default = false

View File

@ -4,6 +4,7 @@
package org.mozilla.gecko.search
import android.app.ActivityManager
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH
@ -36,6 +37,21 @@ class SearchWidgetProvider : AppWidgetProvider() {
override fun onEnabled(context: Context) {
context.settings().addSearchWidgetInstalled(1)
if (isAppInForeground(context)) {
val goHomeOnWidgetAdded = Intent(Intent.ACTION_MAIN)
goHomeOnWidgetAdded.addCategory(Intent.CATEGORY_HOME)
goHomeOnWidgetAdded.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(goHomeOnWidgetAdded)
}
}
// We need this because user can not add widget via launcher app without this
private fun isAppInForeground(context: Context): Boolean {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val runningAppProcesses =
activityManager.runningAppProcesses ?: return false
return runningAppProcesses.any { it.processName == context.packageName &&
it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND }
}
override fun onDeleted(context: Context, appWidgetIds: IntArray) {

View File

@ -0,0 +1,22 @@
<?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="rectangle">
<gradient
android:angle="45"
android:startColor="@color/toolbar_start_gradient_private_theme"
android:centerColor="@color/toolbar_center_gradient_private_theme"
android:endColor="@color/toolbar_end_gradient_private_theme"
android:type="linear" />
</shape>
</item>
<item android:gravity="top">
<shape android:shape="rectangle">
<size android:height="1dp" />
<solid android:color="@color/neutral_faded_private_theme" />
</shape>
</item>
</layer-list>

View File

@ -1,57 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
<?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/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout 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:id="@+id/bookmarks_wrapper"
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/bookmark_list"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bookmarks_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/library_site_item" />
android:layout_height="match_parent">
<TextView
android:id="@+id/bookmarks_empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/bookmarks_empty_message"
android:textColor="?secondaryText"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/bookmark_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/library_site_item" />
<com.google.android.material.button.MaterialButton
android:id="@+id/bookmark_folders_sign_in"
style="@style/NeutralButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/bookmark_sign_in_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bookmark_list"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/bookmarks_empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/bookmarks_empty_message"
android:textColor="?secondaryText"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/bookmarks_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/bookmark_folders_sign_in"
style="@style/NeutralButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/bookmark_sign_in_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bookmark_list"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
android:id="@+id/bookmarks_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -108,7 +108,7 @@
android:id="@+id/tabsTray"
android:layout_width="0dp"
android:layout_height="0dp"
android:paddingBottom="80dp"
android:paddingBottom="140dp"
android:clipToPadding="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View File

@ -39,6 +39,7 @@
android:id="@+id/add_tabs_to_collections_button"
style="@style/PositiveButton"
app:icon="@drawable/ic_tab_collection"
android:visibility="gone"
android:text="@string/tabs_menu_save_to_collection1"
android:layout_marginTop="8dp"/>
</LinearLayout>

View File

@ -74,7 +74,7 @@
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/mozac_browser_tabstray_close"
android:layout_width="44dp"
android:layout_width="48dp"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/close_tab"

View File

@ -188,6 +188,8 @@
<string name="preferences_private_browsing_options">Прыватнае агляданне</string>
<!-- Preference for opening links in a private tab-->
<string name="preferences_open_links_in_a_private_tab">Адкрываць спасылкі ў прыватнай картцы</string>
<!-- Preference for allowing screenshots to be taken while in a private tab-->
<string name="preferences_allow_screenshots_in_private_mode">Дазволіць здымкі экрана ў прыватным рэжыме</string>
<!-- Preference for accessibility -->
<string name="preferences_accessibility">Даступнасць</string>
<!-- Preference category for account information -->
@ -771,6 +773,9 @@
<!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header_preview_installed">Firefox Nightly пераехаў</string>
<!-- text for firefox preview moving tip button -->
<string name="tip_firefox_preview_moved_button_preview_installed">Пераключыцца на новы Nightly</string>
<!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header_preview_not_installed">Firefox Nightly пераехаў</string>
<!-- text for firefox preview moving tip button -->

View File

@ -2,7 +2,7 @@
<!-- 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/. -->
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Normal theme colors for dark mode -->
<color name="primary_text_normal_theme">@color/primary_text_dark_theme</color>
<color name="secondary_text_normal_theme">@color/secondary_text_dark_theme</color>
@ -73,4 +73,7 @@
<!-- Reader View colors -->
<color name="mozac_feature_readerview_text_color">@color/primary_text_dark_theme</color>
<!-- TextInputLayout default Error Color -->
<color name="design_error" tools:override="true">@color/destructive_dark_theme</color>
</resources>

View File

@ -1052,6 +1052,18 @@
<!-- Text shown in snackbar for the "retry" action that the user has after sharing tabs failed -->
<string name="sync_sent_tab_error_snackbar_action">GINÙ HUIN ÑÛ</string>
<!-- Title of QR Pairing Fragment -->
<string name="sync_scan_code">Gānārī ñadu\'ua da\'nga\' kôdigo</string>
<!-- Instructions on how to access pairing -->
<string name="sign_in_instructions"><![CDATA[Gānā\'nïn Firefox riña si āgâ\'t nī gātū riña <b>https://firefox.com/pair</b>]]></string>
<!-- Text shown for sign in pairing when ready -->
<string name="sign_in_ready_for_scan">Ngà huā chrunj da\' gānārij ñadu\'ua</string>
<!-- Text shown for settings option for sign with pairing -->
<string name="sign_in_with_camera">Gāyì\'ì sēsiûn ngà si kamarât</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Gārāsun si kōrreôt si lūgaj</string>
<!-- Option to continue signing out of account shown in confirmation dialog to sign out of account -->
<string name="sign_out_disconnect">Gāhuī riña internet</string>
<!-- Option to cancel signing out shown in confirmation dialog to sign out of account -->
<string name="sign_out_cancel">Dūyichin\'</string>

View File

@ -887,8 +887,6 @@
<string name="preference_summary_delete_browsing_data_on_quit">جب آپ مین مینو سے &quot;چھوڑیں&quot; کو منتخب کرتے ہیں تو براؤزنگ کا ڈیٹا خودبخود حذف ہو جاتا ہے</string>
<!-- Summary for the Delete browsing data on quit preference. "Quit" translation should match delete_browsing_data_on_quit_action translation. -->
<string name="preference_summary_delete_browsing_data_on_quit_2">جب آپ مین مینو سے \&quot;چھوڑیں\&quot; کو منتخب کرتے ہیں تو براؤزنگ کا ڈیٹا خودبخود حذف ہو جاتا ہے</string>
<!-- Category for history items to delete on quit in delete browsing data on quit -->
<string name="preferences_delete_browsing_data_on_quit_browsing_history">براؤزنگ سابقات</string>
<!-- Action item in menu for the Delete browsing data on quit feature -->
<string name="delete_browsing_data_on_quit_action">بند کریں</string>
@ -973,6 +971,11 @@
<!-- text for the private browsing onboarding card header -->
<string name="onboarding_private_browsing_header">رازداری سے براؤز کریں</string>
<!-- text for the private browsing onboarding card description
The first parameter is an icon that represents private browsing -->
<string name="onboarding_private_browsing_description1">ایک نجی ٹیب ایک بار کھولیں: %s آئیکن پر ٹیپ کریں۔</string>
<!-- text for the private browsing onboarding card description, explaining how to always using private browsing -->
<string name="onboarding_private_browsing_always_description">ہر بار نجی ٹیب کھولیں: اپنی نجی براؤزنگ کی ترتیبات کو اپ ڈیٹ کریں۔</string>
<!-- text for the private browsing onbording card button, that launches settings -->
<string name="onboarding_private_browsing_button">سیٹنگز کھولیں</string>
<!-- text for the privacy notice onboarding card header -->
@ -1043,6 +1046,8 @@
<string name="preference_enhanced_tracking_protection_strict_info_button">سخت سراغ کاری تحفظ کے ذریعہ کیا مسدود ہے</string>
<!-- Preference for enhanced tracking protection for the custom protection settings -->
<string name="preference_enhanced_tracking_protection_custom">مخصوص</string>
<!-- Preference description for enhanced tracking protection for the strict protection settings -->
<string name="preference_enhanced_tracking_protection_custom_description_2">منتخب کریں کہ کون سے ٹریکرز اور اسکرپٹ کو مسدود کرنا ہے۔</string>
<!-- Accessibility text for the Strict protection information icon -->
<string name="preference_enhanced_tracking_protection_custom_info_button">مخصوص سراغ کاری تحفظ کے ذریعہ کیا مسدود ہے</string>
<!-- Header for categories that are being blocked by current Enhanced Tracking Protection settings -->
@ -1174,6 +1179,8 @@
<string name="preferences_passwords_sync_logins_sign_in">Sync کے لئے سائن ان کریں</string>
<!-- Preference to access list of saved logins -->
<string name="preferences_passwords_saved_logins">لاگ ان کو محفوظ کیا گیا</string>
<!-- Description of empty list of saved passwords. Placeholder is replaced with app name. -->
<string name="preferences_passwords_saved_logins_description_empty_text">آپ جو لاگ انز محفوظ کرتے ہیں یا %s سے sync کرتے ہیں وہ یہاں دکھائے جائیں گے۔</string>
<!-- Preference to access list of saved logins -->
<string name="preferences_passwords_saved_logins_description_empty_learn_more_link">Sync کے بارے میں مزید معلومات حاصل کریں۔</string>
<!-- Preference to access list of login exceptions that we never save logins for -->

View File

@ -2,7 +2,7 @@
<!-- 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/. -->
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Photon colors -->
<color name="dark_grey_05">#5B5B66</color>
<color name="dark_grey_10">#52525E</color>
@ -370,4 +370,7 @@
<!-- SearchView Hint Color -->
<color name="search_view_hint_color">#5B5B66</color>
<!-- TextInputLayout default Error Color -->
<color name="design_error" tools:override="true">@color/destructive_light_theme</color>
</resources>

View File

@ -54,7 +54,6 @@
<string name="pref_key_experimentation" translatable="false">pref_key_experimentation</string>
<string name="pref_key_showed_private_mode_cfr" translatable="false">pref_key_showed_private_mode_cfr</string>
<string name="pref_key_private_mode_opened" translatable="false">pref_key_private_mode_opened</string>
<string name="pref_key_reader_mode_opened" translatable="false">pref_key_reader_mode_opened</string>
<string name="pref_key_open_in_app_opened" translatable="false">pref_key_open_in_app_opened</string>
<string name="pref_key_install_pwa_opened" translatable="false">pref_key_install_pwa_opened</string>

View File

@ -5,48 +5,167 @@ import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.verify
import io.mockk.verifyAll
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.MediaState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.TabsUseCases
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.components.Analytics
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.home.Tab
@ExperimentalCoroutinesApi
class DefaultCollectionCreationControllerTest {
private val testCoroutineScope = TestCoroutineScope()
private lateinit var state: CollectionCreationState
private lateinit var controller: DefaultCollectionCreationController
@MockK(relaxed = true) private lateinit var store: CollectionCreationStore
@MockK(relaxed = true) private lateinit var dismiss: () -> Unit
@MockK(relaxed = true) private lateinit var analytics: Analytics
@MockK private lateinit var tabCollectionStorage: TabCollectionStorage
@MockK private lateinit var tabsUseCases: TabsUseCases
@MockK(relaxUnitFun = true) private lateinit var metrics: MetricController
@MockK(relaxUnitFun = true) private lateinit var tabCollectionStorage: TabCollectionStorage
@MockK private lateinit var sessionManager: SessionManager
@MockK private lateinit var state: CollectionCreationState
@Before
fun before() {
MockKAnnotations.init(this)
every { store.state } returns state
every { state.tabCollections } returns emptyList()
every { state.tabs } returns emptyList()
state = CollectionCreationState(
tabCollections = emptyList(),
tabs = emptyList()
)
every { store.state } answers { state }
controller = DefaultCollectionCreationController(
store, dismiss, analytics,
store, dismiss, metrics,
tabCollectionStorage, sessionManager, testCoroutineScope
)
}
@Test
fun `GIVEN tab list WHEN saveCollectionName is called THEN collection should be created`() {
val session = mockSession(sessionId = "session-1")
val sessions = listOf(
session,
mockSession(sessionId = "session-2")
)
every { sessionManager.findSessionById("session-1") } returns session
every { sessionManager.findSessionById("null-session") } returns null
every { sessionManager.sessions } returns sessions
val tabs = listOf(
Tab("session-1", "", "", "", mediaState = MediaState.State.NONE),
Tab("null-session", "", "", "", mediaState = MediaState.State.NONE)
)
controller.saveCollectionName(tabs, "name")
verify { dismiss() }
verify { tabCollectionStorage.createCollection("name", listOf(session)) }
verify { metrics.track(Event.CollectionSaved(2, 1)) }
}
@Test
fun `GIVEN name collection WHEN backPressed is called THEN next step should be dispatched`() {
state = state.copy(tabCollections = listOf(mockk()))
controller.backPressed(SaveCollectionStep.NameCollection)
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.SelectCollection)) }
state = state.copy(tabCollections = emptyList(), tabs = listOf(mockk(), mockk()))
controller.backPressed(SaveCollectionStep.NameCollection)
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.SelectTabs)) }
state = state.copy(tabCollections = emptyList(), tabs = listOf(mockk()))
controller.backPressed(SaveCollectionStep.NameCollection)
verify { dismiss() }
}
@Test
fun `GIVEN select collection WHEN backPressed is called THEN next step should be dispatched`() {
state = state.copy(tabCollections = emptyList(), tabs = listOf(mockk(), mockk()))
controller.backPressed(SaveCollectionStep.SelectCollection)
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.SelectTabs)) }
state = state.copy(tabCollections = emptyList(), tabs = listOf(mockk()))
controller.backPressed(SaveCollectionStep.SelectCollection)
verify { dismiss() }
}
@Test
fun `GIVEN last step WHEN backPressed is called THEN dismiss should be called`() {
controller.backPressed(SaveCollectionStep.SelectTabs)
verify { dismiss() }
controller.backPressed(SaveCollectionStep.RenameCollection)
verify { dismiss() }
}
@Test
fun `GIVEN collection WHEN renameCollection is called THEN collection should be renamed`() = testCoroutineScope.runBlockingTest {
val collection = mockk<TabCollection>()
controller.renameCollection(collection, "name")
advanceUntilIdle()
verifyAll {
dismiss()
tabCollectionStorage.renameCollection(collection, "name")
metrics.track(Event.CollectionRenamed)
}
}
@Test
fun `WHEN select all is called THEN add all should be dispatched`() {
controller.selectAllTabs()
verify { store.dispatch(CollectionCreationAction.AddAllTabs) }
controller.deselectAllTabs()
verify { store.dispatch(CollectionCreationAction.RemoveAllTabs) }
controller.close()
verify { dismiss() }
}
@Test
fun `WHEN select tab is called THEN add tab should be dispatched`() {
val tab = mockk<Tab>()
controller.addTabToSelection(tab)
verify { store.dispatch(CollectionCreationAction.TabAdded(tab)) }
controller.removeTabFromSelection(tab)
verify { store.dispatch(CollectionCreationAction.TabRemoved(tab)) }
}
@Test
fun `WHEN selectCollection is called THEN add tabs should be added to collection`() {
val session = mockSession(sessionId = "session-1")
val sessions = listOf(
session,
mockSession(sessionId = "session-2")
)
every { sessionManager.findSessionById("session-1") } returns session
every { sessionManager.sessions } returns sessions
val tabs = listOf(
Tab("session-1", "", "", "", mediaState = MediaState.State.NONE)
)
val collection = mockk<TabCollection>()
controller.selectCollection(collection, tabs)
verify { dismiss() }
verify { tabCollectionStorage.addTabsToCollection(collection, listOf(session)) }
verify { metrics.track(Event.CollectionTabsAdded(2, 1)) }
}
@Test
fun `GIVEN previous step was SelectTabs or RenameCollection WHEN stepBack is called THEN null should be returned`() {
assertNull(controller.stepBack(SaveCollectionStep.SelectTabs))
@ -55,83 +174,79 @@ class DefaultCollectionCreationControllerTest {
@Test
fun `GIVEN previous step was SelectCollection AND more than one tab is open WHEN stepBack is called THEN SelectTabs should be returned`() {
every { state.tabs } returns listOf(mockk(), mockk())
state = state.copy(tabs = listOf(mockk(), mockk()))
assertEquals(SaveCollectionStep.SelectTabs, controller.stepBack(SaveCollectionStep.SelectCollection))
}
@Test
fun `GIVEN previous step was SelectCollection AND one or fewer tabs are open WHEN stepbback is called THEN null should be returned`() {
every { state.tabs } returns listOf(mockk())
state = state.copy(tabs = listOf(mockk()))
assertNull(controller.stepBack(SaveCollectionStep.SelectCollection))
every { state.tabs } returns emptyList()
state = state.copy(tabs = emptyList())
assertNull(controller.stepBack(SaveCollectionStep.SelectCollection))
}
@Test
fun `GIVEN previous step was NameCollection AND tabCollections is empty AND more than one tab is open WHEN stepBack is called THEN SelectTabs should be returned`() {
every { state.tabCollections } returns emptyList()
every { state.tabs } returns listOf(mockk(), mockk())
state = state.copy(tabCollections = emptyList(), tabs = listOf(mockk(), mockk()))
assertEquals(SaveCollectionStep.SelectTabs, controller.stepBack(SaveCollectionStep.NameCollection))
}
@Test
fun `GIVEN previous step was NameCollection AND tabCollections is empty AND one or fewer tabs are open WHEN stepBack is called THEN null should be returned`() {
every { state.tabCollections } returns emptyList()
every { state.tabs } returns listOf(mockk())
state = state.copy(tabCollections = emptyList(), tabs = listOf(mockk()))
assertNull(controller.stepBack(SaveCollectionStep.NameCollection))
every { state.tabCollections } returns emptyList()
every { state.tabs } returns emptyList()
state = state.copy(tabCollections = emptyList(), tabs = emptyList())
assertNull(controller.stepBack(SaveCollectionStep.NameCollection))
}
@Test
fun `GIVEN previous step was NameCollection AND tabCollections is not empty WHEN stepBack is called THEN SelectCollection should be returned`() {
every { state.tabCollections } returns listOf(mockk())
state = state.copy(tabCollections = listOf(mockk()))
assertEquals(SaveCollectionStep.SelectCollection, controller.stepBack(SaveCollectionStep.NameCollection))
}
@Test
fun `GIVEN list of collections WHEN default collection number is required THEN return next default number`() {
val collections: MutableList<TabCollection> = ArrayList()
collections.add(mockk {
every { title } returns "Collection 1"
})
collections.add(mockk {
every { title } returns "Collection 2"
})
collections.add(mockk {
every { title } returns "Collection 3"
})
every { state.tabCollections } returns collections
val collections = mutableListOf<TabCollection>(
mockk {
every { title } returns "Collection 1"
},
mockk {
every { title } returns "Collection 2"
},
mockk {
every { title } returns "Collection 3"
}
)
state = state.copy(tabCollections = collections)
assertEquals(4, controller.getDefaultCollectionNumber())
collections.add(mockk {
every { title } returns "Collection 5"
})
state = state.copy(tabCollections = collections)
assertEquals(6, controller.getDefaultCollectionNumber())
collections.add(mockk {
every { title } returns "Random name"
})
state = state.copy(tabCollections = collections)
assertEquals(6, controller.getDefaultCollectionNumber())
collections.add(mockk {
every { title } returns "Collection 10 10"
})
state = state.copy(tabCollections = collections)
assertEquals(6, controller.getDefaultCollectionNumber())
}
@Test
fun `WHEN adding a new collection THEN dispatch NameCollection step changed`() {
val collections: List<TabCollection> = ArrayList()
every { state.tabCollections } returns collections
controller.addNewCollection()
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.NameCollection, 1)) }
@ -139,9 +254,6 @@ class DefaultCollectionCreationControllerTest {
@Test
fun `GIVEN empty list of collections WHEN saving tabs to collection THEN dispatch NameCollection step changed`() {
val collections: List<TabCollection> = ArrayList()
every { state.tabCollections } returns collections
controller.saveTabsToCollection(ArrayList())
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.NameCollection, 1)) }
@ -149,14 +261,14 @@ class DefaultCollectionCreationControllerTest {
@Test
fun `GIVEN list of collections WHEN saving tabs to collection THEN dispatch NameCollection step changed`() {
val collections: MutableList<TabCollection> = ArrayList()
collections.add(mockk {
every { title } returns "Collection 1"
})
collections.add(mockk {
every { title } returns "Random Collection"
})
every { state.tabCollections } returns collections
state = state.copy(tabCollections = listOf(
mockk {
every { title } returns "Collection 1"
},
mockk {
every { title } returns "Random Collection"
}
))
controller.saveTabsToCollection(ArrayList())
@ -165,27 +277,32 @@ class DefaultCollectionCreationControllerTest {
@Test
fun `normalSessionSize only counts non-private non-custom sessions`() {
fun session(isPrivate: Boolean, isCustom: Boolean) = mockk<Session>().apply {
every { private } returns isPrivate
every { isCustomTabSession() } returns isCustom
}
val normal1 = mockSession()
val normal2 = mockSession()
val normal3 = mockSession()
val normal1 = session(isPrivate = false, isCustom = false)
val normal2 = session(isPrivate = false, isCustom = false)
val normal3 = session(isPrivate = false, isCustom = false)
val private1 = mockSession(isPrivate = true)
val private2 = mockSession(isPrivate = true)
val private1 = session(isPrivate = true, isCustom = false)
val private2 = session(isPrivate = true, isCustom = false)
val custom1 = mockSession(isCustom = true)
val custom2 = mockSession(isCustom = true)
val custom3 = mockSession(isCustom = true)
val custom1 = session(isPrivate = false, isCustom = true)
val custom2 = session(isPrivate = false, isCustom = true)
val custom3 = session(isPrivate = false, isCustom = true)
val privateCustom = session(isPrivate = true, isCustom = true)
val privateCustom = mockSession(isPrivate = true, isCustom = true)
every { sessionManager.sessions } returns listOf(normal1, private1, private2, custom1,
normal2, normal3, custom2, custom3, privateCustom)
assertEquals(3, controller.normalSessionSize(sessionManager))
}
private fun mockSession(
sessionId: String? = null,
isPrivate: Boolean = false,
isCustom: Boolean = false
) = mockk<Session> {
sessionId?.let { every { id } returns it }
every { private } returns isPrivate
every { isCustomTabSession() } returns isCustom
}
}

View File

@ -16,7 +16,7 @@ import org.junit.Test
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.Settings
internal class InstallationPingTest {
internal class FirstSessionPingTest {
@Test
fun `checkAndSend() triggers the ping if it wasn't marked as triggered`() {
@ -24,7 +24,7 @@ internal class InstallationPingTest {
val mockedSettings: Settings = mockk(relaxed = true)
mockkStatic("org.mozilla.fenix.ext.ContextKt")
every { mockedContext.settings() } returns mockedSettings
val mockAp = spyk(InstallationPing(mockedContext), recordPrivateCalls = true)
val mockAp = spyk(FirstSessionPing(mockedContext), recordPrivateCalls = true)
every { mockAp.checkMetricsNotEmpty() } returns true
every { mockAp.wasAlreadyTriggered() } returns false
every { mockAp.markAsTriggered() } just Runs
@ -39,7 +39,7 @@ internal class InstallationPingTest {
@Test
fun `checkAndSend() doesn't trigger the ping again if it was marked as triggered`() {
val mockAp = spyk(InstallationPing(mockk()), recordPrivateCalls = true)
val mockAp = spyk(FirstSessionPing(mockk()), recordPrivateCalls = true)
every { mockAp.wasAlreadyTriggered() } returns true
mockAp.checkAndSend()

View File

@ -62,13 +62,4 @@ class BrowserInteractorTest {
verify { browserToolbarController.handleToolbarItemInteraction(item) }
}
@Test
fun onBrowserMenuDismissed() {
val itemList: List<ToolbarMenu.Item> = listOf()
interactor.onBrowserMenuDismissed(itemList)
verify { browserToolbarController.handleBrowserMenuDismissed(itemList) }
}
}

View File

@ -60,7 +60,6 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.toTab
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.home.Tab
@ -185,22 +184,6 @@ class DefaultBrowserToolbarControllerTest {
verify { onTabCounterClicked() }
}
@Test
fun `handle BrowserMenu dismissed with all options available`() = runBlockingTest {
val itemList: List<ToolbarMenu.Item> = listOf(
ToolbarMenu.Item.AddToHomeScreen,
ToolbarMenu.Item.OpenInApp
)
val activity = HomeActivity()
val controller = createController(scope = this, activity = activity)
controller.handleBrowserMenuDismissed(itemList)
assertEquals(true, activity.settings().installPwaOpened)
assertEquals(true, activity.settings().openInAppOpened)
}
@Test
fun handleToolbarClick() = runBlockingTest {
every { currentSession.id } returns "1"

View File

@ -1,109 +0,0 @@
package org.mozilla.fenix.components.toolbar
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.spyk
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.ReaderState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.storage.BookmarksStorage
import mozilla.components.feature.app.links.AppLinkRedirect
import mozilla.components.feature.app.links.AppLinksUseCases
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.Settings
class DefaultToolbarMenuTest {
private lateinit var defaultToolbarMenu: DefaultToolbarMenu
private val context: Context = mockk(relaxed = true)
private val sessionManager: SessionManager = mockk(relaxed = true)
private val onItemTapped: (ToolbarMenu.Item) -> Unit = {}
private val lifecycleOwner: LifecycleOwner = mockk(relaxed = true)
private val bookmarksStorage: BookmarksStorage = mockk(relaxed = true)
private val store: BrowserStore = BrowserStore(initialState = BrowserState(
listOf(
createTab("https://www.mozilla.org", id = "readerable-tab", readerState = ReaderState(readerable = true))
)
))
@Before
fun setUp() {
defaultToolbarMenu = spyk(
DefaultToolbarMenu(
context,
sessionManager,
store,
hasAccountProblem = false,
shouldReverseItems = false,
onItemTapped = onItemTapped,
lifecycleOwner = lifecycleOwner,
bookmarksStorage = bookmarksStorage
)
)
val settings = Settings.getInstance(context, true)
mockkStatic("org.mozilla.fenix.ext.ContextKt")
every { context.settings() } returns settings
}
@Test
fun `get all low prio highlight items`() {
every { context.components.useCases.webAppUseCases.isPinningSupported() } returns true
every { context.components.useCases.webAppUseCases.isInstallable() } returns true
val getAppLinkRedirect: AppLinksUseCases.GetAppLinkRedirect = mockk(relaxed = true)
every { context.components.useCases.appLinksUseCases.appLinkRedirect } returns getAppLinkRedirect
val appLinkRedirect: AppLinkRedirect = mockk(relaxed = true)
every { appLinkRedirect.hasExternalApp() } returns true
every { getAppLinkRedirect(any()) } returns appLinkRedirect
val list = defaultToolbarMenu.getLowPrioHighlightItems()
assertEquals(ToolbarMenu.Item.InstallToHomeScreen, list[0])
assertEquals(ToolbarMenu.Item.OpenInApp, list[1])
}
@Test
fun `get all low prio highlight items without AddToHomeScreen`() {
val settings = Settings.getInstance(context, true)
mockkStatic("org.mozilla.fenix.ext.ContextKt")
every { context.settings() } returns settings
every { context.components.useCases.webAppUseCases.isPinningSupported() } returns false
val getAppLinkRedirect: AppLinksUseCases.GetAppLinkRedirect = mockk(relaxed = true)
every { context.components.useCases.appLinksUseCases.appLinkRedirect } returns getAppLinkRedirect
val appLinkRedirect: AppLinkRedirect = mockk(relaxed = true)
every { appLinkRedirect.hasExternalApp() } returns true
every { getAppLinkRedirect(any()) } returns appLinkRedirect
val list = defaultToolbarMenu.getLowPrioHighlightItems()
assertEquals(ToolbarMenu.Item.OpenInApp, list[0])
}
@Test
fun `get all low prio highlight items without OpenInApp`() {
every { context.components.useCases.webAppUseCases.isPinningSupported() } returns true
every { context.components.useCases.webAppUseCases.isInstallable() } returns true
val getAppLinkRedirect: AppLinksUseCases.GetAppLinkRedirect = mockk(relaxed = true)
every { context.components.useCases.appLinksUseCases.appLinkRedirect } returns getAppLinkRedirect
every { getAppLinkRedirect(any()).hasExternalApp() } returns false
val list = defaultToolbarMenu.getLowPrioHighlightItems()
assertEquals(ToolbarMenu.Item.InstallToHomeScreen, list[0])
}
}

View File

@ -12,16 +12,24 @@ import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import io.mockk.Runs
import io.mockk.called
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.slot
import io.mockk.spyk
import io.mockk.verify
import io.mockk.verifyOrder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
@ -31,15 +39,21 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.Services
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.bookmarkStorage
import org.mozilla.fenix.ext.components
@SuppressWarnings("TooManyFunctions", "LargeClass")
@ExperimentalCoroutinesApi
class BookmarkControllerTest {
private lateinit var controller: BookmarkController
private val bookmarkStore = spyk(BookmarkFragmentStore(BookmarkFragmentState(null)))
private val context: Context = mockk(relaxed = true)
private val scope = TestCoroutineScope()
private val navController: NavController = mockk(relaxed = true)
private val sharedViewModel: BookmarksSharedViewModel = mockk()
private val loadBookmarkNode: suspend (String) -> BookmarkNode? = mockk(relaxed = true)
private val showSnackbar: (String) -> Unit = mockk(relaxed = true)
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit = mockk(relaxed = true)
private val deleteBookmarkFolder: (BookmarkNode) -> Unit = mockk(relaxed = true)
@ -87,10 +101,16 @@ class BookmarkControllerTest {
every { navController.currentDestination } returns NavDestination("").apply {
id = R.id.bookmarkFragment
}
every { bookmarkStore.dispatch(any()) } returns mockk()
every { sharedViewModel.selectedFolder = any() } just runs
controller = DefaultBookmarkController(
context = homeActivity,
navController = navController,
scope = scope,
store = bookmarkStore,
sharedViewModel = sharedViewModel,
loadBookmarkNode = loadBookmarkNode,
showSnackbar = showSnackbar,
deleteBookmarkNodes = deleteBookmarkNodes,
deleteBookmarkFolder = deleteBookmarkFolder,
@ -98,6 +118,21 @@ class BookmarkControllerTest {
)
}
@After
fun cleanUp() {
scope.cleanupTestCoroutines()
}
@Test
fun `handleBookmarkChanged updates the selected bookmark node`() {
controller.handleBookmarkChanged(tree)
verify {
sharedViewModel.selectedFolder = tree
bookmarkStore.dispatch(BookmarkFragmentAction.Change(tree))
}
}
@Test
fun `handleBookmarkTapped should load the bookmark in a new tab`() {
controller.handleBookmarkTapped(item)
@ -124,15 +159,27 @@ class BookmarkControllerTest {
}
@Test
fun `handleBookmarkExpand should navigate to the 'Bookmark' fragment`() {
fun `handleBookmarkExpand clears selection and invokes pending deletions`() {
coEvery { loadBookmarkNode.invoke(any()) } returns tree
controller.handleBookmarkExpand(tree)
verify {
invokePendingDeletion.invoke()
navController.navigate(
BookmarkFragmentDirections.actionBookmarkFragmentSelf(tree.guid),
null
)
controller.handleAllBookmarksDeselected()
}
}
@Test
fun `handleBookmarkExpand should refresh and change the active bookmark node`() {
coEvery { loadBookmarkNode.invoke(any()) } returns tree
controller.handleBookmarkExpand(tree)
coVerify {
loadBookmarkNode.invoke(tree.guid)
sharedViewModel.selectedFolder = tree
bookmarkStore.dispatch(BookmarkFragmentAction.Change(tree))
}
}
@ -161,7 +208,16 @@ class BookmarkControllerTest {
}
@Test
fun `handleBookmarkSelected should show a toast when selecting a folder`() {
fun `handleBookmarkSelected dispatches Select action when selecting a non-root folder`() {
controller.handleBookmarkSelected(item)
verify {
bookmarkStore.dispatch(BookmarkFragmentAction.Select(item))
}
}
@Test
fun `handleBookmarkSelected should show a toast when selecting a root folder`() {
val errorMessage = context.getString(R.string.bookmark_cannot_edit_root)
controller.handleBookmarkSelected(root)
@ -171,6 +227,24 @@ class BookmarkControllerTest {
}
}
@Test
fun `handleBookmarkSelected does not select in Syncing mode`() {
every { bookmarkStore.state.mode } returns BookmarkFragmentState.Mode.Syncing
controller.handleBookmarkSelected(item)
verify { bookmarkStore.dispatch(BookmarkFragmentAction.Select(item)) wasNot called }
}
@Test
fun `handleBookmarkDeselected dispatches Deselect action`() {
controller.handleBookmarkDeselected(item)
verify {
bookmarkStore.dispatch(BookmarkFragmentAction.Deselect(item))
}
}
@Test
fun `handleCopyUrl should copy bookmark url to clipboard and show a toast`() {
val clipboardManager: ClipboardManager = mockk(relaxed = true)
@ -248,7 +322,24 @@ class BookmarkControllerTest {
}
@Test
fun `handleBackPressed should trigger handleBackPressed in NavController`() {
fun `handleRequestSync dispatches actions in the correct order`() {
every { homeActivity.components.backgroundServices.accountManager } returns mockk(relaxed = true)
coEvery { homeActivity.bookmarkStorage.getBookmark(any()) } returns tree
coEvery { loadBookmarkNode.invoke(any()) } returns tree
controller.handleRequestSync()
verifyOrder {
bookmarkStore.dispatch(BookmarkFragmentAction.StartSync)
bookmarkStore.dispatch(BookmarkFragmentAction.FinishSync)
}
}
@Test
fun `handleBackPressed with one item in backstack should trigger handleBackPressed in NavController`() {
every { bookmarkStore.state.guidBackstack } returns listOf(tree.guid)
every { bookmarkStore.state.tree } returns tree
controller.handleBackPressed()
verify {

View File

@ -4,10 +4,7 @@
package org.mozilla.fenix.library.bookmarks
import io.mockk.called
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import io.mockk.verifyOrder
import mozilla.appservices.places.BookmarkRoot
@ -24,8 +21,6 @@ class BookmarkFragmentInteractorTest {
private lateinit var interactor: BookmarkFragmentInteractor
private val bookmarkStore = spyk(BookmarkFragmentStore(BookmarkFragmentState(null)))
private val sharedViewModel: BookmarksSharedViewModel = mockk(relaxed = true)
private val bookmarkController: DefaultBookmarkController = mockk(relaxed = true)
private val metrics: MetricController = mockk(relaxed = true)
@ -41,12 +36,8 @@ class BookmarkFragmentInteractorTest {
@Before
fun setup() {
every { bookmarkStore.dispatch(any()) } returns mockk()
interactor =
BookmarkFragmentInteractor(
bookmarkStore = bookmarkStore,
viewModel = sharedViewModel,
bookmarksController = bookmarkController,
metrics = metrics
)
@ -57,7 +48,7 @@ class BookmarkFragmentInteractorTest {
interactor.onBookmarksChanged(tree)
verify {
bookmarkStore.dispatch(BookmarkFragmentAction.Change(tree))
bookmarkController.handleBookmarkChanged(tree)
}
}
@ -108,7 +99,7 @@ class BookmarkFragmentInteractorTest {
interactor.select(item)
verify {
bookmarkStore.dispatch(BookmarkFragmentAction.Select(item))
bookmarkController.handleBookmarkSelected(item)
}
}
@ -117,7 +108,7 @@ class BookmarkFragmentInteractorTest {
interactor.deselect(item)
verify {
bookmarkStore.dispatch(BookmarkFragmentAction.Deselect(item))
bookmarkController.handleBookmarkDeselected(item)
}
}
@ -126,17 +117,10 @@ class BookmarkFragmentInteractorTest {
interactor.onAllBookmarksDeselected()
verify {
bookmarkStore.dispatch(BookmarkFragmentAction.DeselectAll)
bookmarkController.handleAllBookmarksDeselected()
}
}
@Test
fun `cannot select bookmark roots`() {
interactor.select(root)
verify { bookmarkStore wasNot called }
}
@Test
fun `copy a bookmark item`() {
interactor.onCopyPressed(item)
@ -217,4 +201,13 @@ class BookmarkFragmentInteractorTest {
bookmarkController.handleBackPressed()
}
}
@Test
fun `request a sync`() {
interactor.onRequestSync()
verify {
bookmarkController.handleRequestSync()
}
}
}

View File

@ -41,6 +41,30 @@ class BookmarkFragmentStoreTest {
assertEquals(store.state.mode, initialState.mode)
}
@Test
fun `changing the tree of bookmarks adds the tree to the visited nodes`() = runBlocking {
val initialState = BookmarkFragmentState(null)
val store = BookmarkFragmentStore(initialState)
store.dispatch(BookmarkFragmentAction.Change(tree)).join()
store.dispatch(BookmarkFragmentAction.Change(subfolder)).join()
assertEquals(listOf(tree.guid, subfolder.guid), store.state.guidBackstack)
}
@Test
fun `changing to a node that is in the backstack removes backstack items after that node`() = runBlocking {
val initialState = BookmarkFragmentState(
null,
guidBackstack = listOf(tree.guid, subfolder.guid, item.guid)
)
val store = BookmarkFragmentStore(initialState)
store.dispatch(BookmarkFragmentAction.Change(tree)).join()
assertEquals(listOf(tree.guid), store.state.guidBackstack)
}
@Test
fun `change the tree of bookmarks to the same value`() = runBlocking {
val initialState = BookmarkFragmentState(tree)
@ -177,6 +201,19 @@ class BookmarkFragmentStoreTest {
assertEquals(store.state.mode, BookmarkFragmentState.Mode.Normal(false))
}
@Test
fun `changing the tree or deselecting in Syncing mode should stay in Syncing mode`() = runBlocking {
val initialState = BookmarkFragmentState(tree)
val store = BookmarkFragmentStore(initialState)
store.dispatch(BookmarkFragmentAction.StartSync).join()
store.dispatch(BookmarkFragmentAction.Change(childItem))
assertEquals(BookmarkFragmentState.Mode.Syncing, store.state.mode)
store.dispatch(BookmarkFragmentAction.DeselectAll).join()
assertEquals(BookmarkFragmentState.Mode.Syncing, store.state.mode)
}
private val item = BookmarkNode(BookmarkNodeType.ITEM, "456", "123", 0, "Mozilla", "http://mozilla.org", null)
private val separator = BookmarkNode(BookmarkNodeType.SEPARATOR, "789", "123", 1, null, null, null)
private val subfolder = BookmarkNode(BookmarkNodeType.FOLDER, "987", "123", 0, "Subfolder", null, listOf())

View File

@ -4,7 +4,6 @@
package org.mozilla.fenix.search
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.every
@ -32,7 +31,6 @@ import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.searchEngineManager
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.search.DefaultSearchController.Companion.KEYBOARD_ANIMATION_DELAY
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.whatsnew.clear
@ -49,7 +47,6 @@ class DefaultSearchControllerTest {
private val searchEngine: SearchEngine = mockk(relaxed = true)
private val metrics: MetricController = mockk(relaxed = true)
private val sessionManager: SessionManager = mockk(relaxed = true)
private val lifecycleScope: LifecycleCoroutineScope = mockk(relaxed = true)
private val clearToolbarFocus: (() -> Unit) = mockk(relaxed = true)
private lateinit var controller: DefaultSearchController
@ -67,7 +64,6 @@ class DefaultSearchControllerTest {
activity = activity,
store = store,
navController = navController,
viewLifecycleScope = lifecycleScope,
clearToolbarFocus = clearToolbarFocus
)
@ -123,17 +119,13 @@ class DefaultSearchControllerTest {
activity = activity,
store = store,
navController = navController,
viewLifecycleScope = this,
clearToolbarFocus = clearToolbarFocus
)
controller.handleEditingCancelled()
advanceTimeBy(KEYBOARD_ANIMATION_DELAY)
verify {
clearToolbarFocus()
navController.popBackStack()
}
}

View File

@ -4,249 +4,84 @@
package org.mozilla.fenix.search
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.search.SearchEngineManager
import mozilla.components.browser.session.Session
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore.PREF_FILE_SEARCH_ENGINES
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.searchEngineManager
@ExperimentalCoroutinesApi
class SearchInteractorTest {
private val lifecycleScope: LifecycleCoroutineScope = mockk(relaxed = true)
private val clearToolbarFocus = { }
lateinit var searchController: DefaultSearchController
lateinit var interactor: SearchInteractor
@Before
fun setup() {
searchController = mockk(relaxed = true)
interactor = SearchInteractor(
searchController
)
}
@Test
fun onUrlCommitted() {
val context: HomeActivity = mockk(relaxed = true)
val store: SearchFragmentStore = mockk()
val state: SearchFragmentState = mockk()
val searchEngineManager: SearchEngineManager = mockk(relaxed = true)
val searchEngine = SearchEngineSource.Default(mockk(relaxed = true))
val searchAccessPoint: Event.PerformedSearch.SearchAccessPoint = mockk(relaxed = true)
every { context.metrics } returns mockk(relaxed = true)
every { context.searchEngineManager } returns searchEngineManager
every { context.openToBrowserAndLoad(any(), any(), any(), any(), any(), any()) } just Runs
every { store.state } returns state
every { state.session } returns null
every { state.searchEngineSource } returns searchEngine
every { state.searchAccessPoint } returns searchAccessPoint
every {
context.getSharedPreferences(
PREF_FILE_SEARCH_ENGINES,
Context.MODE_PRIVATE
)
} returns mockk(relaxed = true)
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk(),
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
interactor.onUrlCommitted("test")
verify {
context.openToBrowserAndLoad(
searchTermOrURL = "test",
newTab = true,
from = BrowserDirection.FromSearch,
engine = searchEngine.searchEngine
)
searchController.handleUrlCommitted("test")
}
}
@Test
fun onEditingCanceled() = runBlockingTest {
val navController: NavController = mockk(relaxed = true)
val store: SearchFragmentStore = mockk(relaxed = true)
every { store.state } returns mockk(relaxed = true)
val searchController: SearchController = DefaultSearchController(
mockk(),
store,
navController,
this,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
interactor.onEditingCanceled()
advanceTimeBy(DefaultSearchController.KEYBOARD_ANIMATION_DELAY)
verify {
clearToolbarFocus()
navController.popBackStack()
searchController.handleEditingCancelled()
}
}
@Test
fun onTextChanged() {
val store: SearchFragmentStore = mockk(relaxed = true)
val context: HomeActivity = mockk(relaxed = true)
every { store.state } returns mockk(relaxed = true)
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk(),
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
interactor.onTextChanged("test")
verify { store.dispatch(SearchFragmentAction.UpdateQuery("test")) }
verify { store.dispatch(SearchFragmentAction.AllowSearchSuggestionsInPrivateModePrompt(false)) }
verify { searchController.handleTextChanged("test") }
}
@Test
fun onUrlTapped() {
val context: HomeActivity = mockk()
val store: SearchFragmentStore = mockk()
val state: SearchFragmentState = mockk()
val searchEngine = SearchEngineSource.Default(mockk(relaxed = true))
every { context.metrics } returns mockk(relaxed = true)
every { context.openToBrowserAndLoad(any(), any(), any()) } just Runs
every { store.state } returns state
every { state.session } returns null
every { state.searchEngineSource } returns searchEngine
every {
context.getSharedPreferences(
PREF_FILE_SEARCH_ENGINES,
Context.MODE_PRIVATE
)
} returns mockk(relaxed = true)
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk(),
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
interactor.onUrlTapped("test")
verify {
context.openToBrowserAndLoad(
"test",
true,
BrowserDirection.FromSearch
)
searchController.handleUrlTapped("test")
}
}
@Test
fun onSearchTermsTapped() {
val context: HomeActivity = mockk(relaxed = true)
val store: SearchFragmentStore = mockk()
val state: SearchFragmentState = mockk()
val searchEngineManager: SearchEngineManager = mockk(relaxed = true)
val searchEngine = SearchEngineSource.Default(mockk(relaxed = true))
val searchAccessPoint: Event.PerformedSearch.SearchAccessPoint = mockk(relaxed = true)
every { context.metrics } returns mockk(relaxed = true)
every { context.searchEngineManager } returns searchEngineManager
every { context.openToBrowserAndLoad(any(), any(), any(), any(), any(), any()) } just Runs
every { store.state } returns state
every { state.session } returns null
every { state.searchEngineSource } returns searchEngine
every { state.searchAccessPoint } returns searchAccessPoint
every {
context.getSharedPreferences(
PREF_FILE_SEARCH_ENGINES,
Context.MODE_PRIVATE
)
} returns mockk(relaxed = true)
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk(),
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
interactor.onSearchTermsTapped("test")
verify {
context.openToBrowserAndLoad(
searchTermOrURL = "test",
newTab = true,
from = BrowserDirection.FromSearch,
engine = searchEngine.searchEngine,
forceSearch = true
)
searchController.handleSearchTermsTapped("test")
}
}
@Test
fun onSearchShortcutEngineSelected() {
val context: HomeActivity = mockk(relaxed = true)
every { context.metrics } returns mockk(relaxed = true)
val store: SearchFragmentStore = mockk(relaxed = true)
val state: SearchFragmentState = mockk(relaxed = true)
every { store.state } returns state
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk(),
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
val searchEngine: SearchEngine = mockk(relaxed = true)
interactor.onSearchShortcutEngineSelected(searchEngine)
verify { store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine)) }
verify { searchController.handleSearchShortcutEngineSelected(searchEngine) }
}
@Test
fun onSearchShortcutsButtonClicked() {
val searchController: SearchController = mockk(relaxed = true)
val interactor = SearchInteractor(searchController)
interactor.onSearchShortcutsButtonClicked()
verify { searchController.handleSearchShortcutsButtonClicked() }
@ -254,58 +89,21 @@ class SearchInteractorTest {
@Test
fun onClickSearchEngineSettings() {
val navController: NavController = mockk()
val store: SearchFragmentStore = mockk()
every { store.state } returns mockk(relaxed = true)
every { navController.currentDestination?.id } returns R.id.searchFragment
val searchController: SearchController = DefaultSearchController(
mockk(),
store,
navController,
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
every { navController.navigate(any() as NavDirections) } just Runs
interactor.onClickSearchEngineSettings()
verify {
navController.navigateSafe(
R.id.searchFragment,
SearchFragmentDirections.actionGlobalSearchEngineFragment()
)
searchController.handleClickSearchEngineSettings()
}
}
@Test
fun onExistingSessionSelected() {
val navController: NavController = mockk(relaxed = true)
val context: HomeActivity = mockk(relaxed = true)
val applicationContext: FenixApplication = mockk(relaxed = true)
every { context.applicationContext } returns applicationContext
val store: SearchFragmentStore = mockk()
every { context.openToBrowser(any(), any()) } just Runs
every { store.state } returns mockk(relaxed = true)
val searchController: SearchController = DefaultSearchController(
context,
store,
navController,
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
val session = Session("http://mozilla.org", false)
interactor.onExistingSessionSelected(session)
verify {
context.openToBrowser(BrowserDirection.FromSearch)
searchController.handleExistingSessionSelected(session)
}
}
}

View File

@ -1,3 +1,7 @@
/* 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.session
import android.content.Context
@ -35,6 +39,7 @@ class NotificationSessionObserverTest {
store = BrowserStore()
every { context.components.core.store } returns store
observer = NotificationSessionObserver(context, notificationService)
NotificationSessionObserver.isStartedFromPrivateShortcut = false
}
@Test
@ -44,7 +49,7 @@ class NotificationSessionObserverTest {
store.dispatch(TabListAction.AddTabAction(privateSession)).join()
observer.start()
verify(exactly = 1) { notificationService.start(context) }
verify(exactly = 1) { notificationService.start(context, false) }
confirmVerified(notificationService)
}
@ -57,10 +62,10 @@ class NotificationSessionObserverTest {
verify { notificationService wasNot Called }
store.dispatch(TabListAction.AddTabAction(normalSession)).join()
verify(exactly = 0) { notificationService.start(context) }
verify(exactly = 0) { notificationService.start(context, false) }
store.dispatch(CustomTabListAction.AddCustomTabAction(customSession)).join()
verify(exactly = 0) { notificationService.start(context) }
verify(exactly = 0) { notificationService.start(context, false) }
}
@Test
@ -74,9 +79,9 @@ class NotificationSessionObserverTest {
verify { notificationService wasNot Called }
store.dispatch(CustomTabListAction.AddCustomTabAction(privateCustomSession)).join()
verify(exactly = 0) { notificationService.start(context) }
verify(exactly = 0) { notificationService.start(context, false) }
store.dispatch(CustomTabListAction.AddCustomTabAction(customSession)).join()
verify(exactly = 0) { notificationService.start(context) }
verify(exactly = 0) { notificationService.start(context, false) }
}
}

View File

@ -0,0 +1,27 @@
/* 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.session
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class SessionNotificationServiceTest {
@Test
fun `Service keeps tracked of started state`() {
assertFalse(SessionNotificationService.started)
SessionNotificationService.start(testContext, false)
assertTrue(SessionNotificationService.started)
SessionNotificationService.stop(testContext)
assertFalse(SessionNotificationService.started)
}
}

View File

@ -0,0 +1,43 @@
/* 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.share
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.recyclerview.widget.LinearLayoutManager
import io.mockk.mockk
import io.mockk.verify
import kotlinx.android.synthetic.main.share_close.view.*
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.share.listadapters.ShareTabsAdapter
@RunWith(FenixRobolectricTestRunner::class)
class ShareCloseViewTest {
private lateinit var container: ViewGroup
private lateinit var interactor: ShareCloseInteractor
@Before
fun setup() {
container = FrameLayout(testContext)
interactor = mockk(relaxUnitFun = true)
}
@Test
fun `binds adapter and close button`() {
ShareCloseView(container, interactor)
assertTrue(container.shared_site_list.layoutManager is LinearLayoutManager)
assertTrue(container.shared_site_list.adapter is ShareTabsAdapter)
container.closeButton.performClick()
verify { interactor.onShareClosed() }
}
}

View File

@ -272,6 +272,22 @@ class ShareControllerTest {
assertEquals(textToShare, controller.getShareText())
}
@Test
fun `getShareText attempts to use original URL for reader pages`() {
val shareData = listOf(
ShareData(url = "moz-extension://eb8df45a-895b-4f3a-896a-c0c71ae4/page.html"),
ShareData(url = "moz-extension://eb8df45a-895b-4f3a-896a-c0c71ae5/page.html?url=url0"),
ShareData(url = "url1")
)
val controller = DefaultShareController(
context, shareData, sendTabUseCases, snackbar, navController,
recentAppStorage, testCoroutineScope, dismiss
)
val expectedShareText = "${shareData[0].url}\n\nurl0\n\n${shareData[2].url}"
assertEquals(expectedShareText, controller.getShareText())
}
@Test
fun `ShareTab#toTabData maps a list of ShareTab to a TabData list`() {
var tabData: List<TabData>

View File

@ -0,0 +1,126 @@
/* 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.share.viewholders
import android.view.LayoutInflater
import io.mockk.Called
import io.mockk.mockk
import io.mockk.verify
import kotlinx.android.synthetic.main.account_share_list_item.view.*
import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.DeviceType
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.share.ShareToAccountDevicesInteractor
import org.mozilla.fenix.share.listadapters.SyncShareOption
@RunWith(FenixRobolectricTestRunner::class)
class AccountDeviceViewHolderTest {
private val baseDevice = Device(
id = "",
displayName = "",
deviceType = DeviceType.UNKNOWN,
isCurrentDevice = true,
lastAccessTime = 0L,
capabilities = emptyList(),
subscriptionExpired = false,
subscription = null
)
private lateinit var viewHolder: AccountDeviceViewHolder
private lateinit var interactor: ShareToAccountDevicesInteractor
@Before
fun setup() {
interactor = mockk(relaxUnitFun = true)
val view = LayoutInflater.from(testContext).inflate(AccountDeviceViewHolder.LAYOUT_ID, null)
viewHolder = AccountDeviceViewHolder(view, interactor)
}
@Test
fun `bind SignIn option`() {
viewHolder.bind(SyncShareOption.SignIn)
assertEquals("Sign in to Sync", viewHolder.itemView.deviceName.text)
viewHolder.itemView.performClick()
verify { interactor.onSignIn() }
assertFalse(viewHolder.itemView.hasOnClickListeners())
}
@Test
fun `bind Reconnect option`() {
viewHolder.bind(SyncShareOption.Reconnect)
assertEquals("Reconnect to Sync", viewHolder.itemView.deviceName.text)
viewHolder.itemView.performClick()
verify { interactor.onReauth() }
assertFalse(viewHolder.itemView.hasOnClickListeners())
}
@Test
fun `bind Offline option`() {
viewHolder.bind(SyncShareOption.Offline)
assertEquals("Offline", viewHolder.itemView.deviceName.text)
viewHolder.itemView.performClick()
verify { interactor wasNot Called }
assertFalse(viewHolder.itemView.hasOnClickListeners())
}
@Test
fun `bind AddNewDevice option`() {
viewHolder.bind(SyncShareOption.AddNewDevice)
assertEquals("Connect another device", viewHolder.itemView.deviceName.text)
viewHolder.itemView.performClick()
verify { interactor.onAddNewDevice() }
assertFalse(viewHolder.itemView.hasOnClickListeners())
}
@Test
fun `bind SendAll option`() {
val devices = listOf<Device>(mockk())
viewHolder.bind(SyncShareOption.SendAll(devices))
assertEquals("Send to all devices", viewHolder.itemView.deviceName.text)
viewHolder.itemView.performClick()
verify { interactor.onShareToAllDevices(devices) }
assertFalse(viewHolder.itemView.hasOnClickListeners())
}
@Test
fun `bind mobile SingleDevice option`() {
val device = baseDevice.copy(
deviceType = DeviceType.MOBILE,
displayName = "Mobile"
)
viewHolder.bind(SyncShareOption.SingleDevice(device))
assertEquals("Mobile", viewHolder.itemView.deviceName.text)
viewHolder.itemView.performClick()
verify { interactor.onShareToDevice(device) }
assertFalse(viewHolder.itemView.hasOnClickListeners())
}
@Test
fun `bind desktop SingleDevice option`() {
val device = baseDevice.copy(
deviceType = DeviceType.DESKTOP,
displayName = "Desktop"
)
viewHolder.bind(SyncShareOption.SingleDevice(device))
assertEquals("Desktop", viewHolder.itemView.deviceName.text)
viewHolder.itemView.performClick()
verify { interactor.onShareToDevice(device) }
assertFalse(viewHolder.itemView.hasOnClickListeners())
}
}

View File

@ -0,0 +1,67 @@
/* 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.share.viewholders
import android.view.LayoutInflater
import androidx.appcompat.content.res.AppCompatResources.getDrawable
import io.mockk.Called
import io.mockk.mockk
import io.mockk.verify
import kotlinx.android.synthetic.main.app_share_list_item.view.*
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.share.ShareToAppsInteractor
import org.mozilla.fenix.share.listadapters.AppShareOption
@RunWith(FenixRobolectricTestRunner::class)
class AppViewHolderTest {
private lateinit var viewHolder: AppViewHolder
private lateinit var interactor: ShareToAppsInteractor
@Before
fun setup() {
interactor = mockk(relaxUnitFun = true)
val view = LayoutInflater.from(testContext).inflate(AppViewHolder.LAYOUT_ID, null)
viewHolder = AppViewHolder(view, interactor)
}
@Test
fun `bind app share option`() {
val app = AppShareOption(
name = "Pocket",
icon = getDrawable(testContext, R.drawable.ic_pocket)!!,
packageName = "com.mozilla.pocket",
activityName = "MainActivity"
)
viewHolder.bind(app)
assertEquals("Pocket", viewHolder.itemView.appName.text)
assertEquals(app.icon, viewHolder.itemView.appIcon.drawable)
}
@Test
fun `trigger interactor if application is bound`() {
val app = AppShareOption(
name = "Pocket",
icon = getDrawable(testContext, R.drawable.ic_pocket)!!,
packageName = "com.mozilla.pocket",
activityName = "MainActivity"
)
viewHolder.itemView.performClick()
verify { interactor wasNot Called }
viewHolder.bind(app)
viewHolder.itemView.performClick()
verify { interactor.onShareToApp(app) }
}
}

View File

@ -304,19 +304,6 @@ class SettingsTest {
assertFalse(settings.shouldUseTrackingProtection)
}
@Test
fun shouldSetReaderModeOpened() {
// When
// Then
assertFalse(settings.readerModeOpened)
// When
settings.readerModeOpened = true
// Then
assertTrue(settings.readerModeOpened)
}
@Test
fun shouldSetOpenInAppOpened() {
// When

View File

@ -3,5 +3,5 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
object AndroidComponents {
const val VERSION = "47.0.20200623130149"
const val VERSION = "48.0.20200625130125"
}

View File

@ -116,6 +116,8 @@ object Deps {
const val mozilla_feature_webnotifications = "org.mozilla.components:feature-webnotifications:${Versions.mozilla_android_components}"
const val mozilla_feature_webcompat_reporter = "org.mozilla.components:feature-webcompat-reporter:${Versions.mozilla_android_components}"
const val mozilla_service_digitalassetlinks =
"org.mozilla.components:service-digitalassetlinks:${Versions.mozilla_android_components}"
const val mozilla_service_experiments =
"org.mozilla.components:service-experiments:${Versions.mozilla_android_components}"
const val mozilla_service_sync_logins =

View File

@ -9,7 +9,7 @@ This means you might have to go searching through the dependency tree to get a f
- [activation](#activation)
- [events](#events)
- [installation](#installation)
- [first-session](#first-session)
- [metrics](#metrics)
- [startup-timeline](#startup-timeline)
@ -82,6 +82,10 @@ The following metrics are added to the ping:
| collections.tab_select_opened |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened the select tabs screen (the first step of the collection creation flow) |[1](https://github.com/mozilla-mobile/fenix/pull/3935)||2020-09-01 |
| collections.tabs_added |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user saved a list of tabs to an existing collection |[1](https://github.com/mozilla-mobile/fenix/pull/3935)|<ul><li>tabs_open: The number of tabs open in the current session</li><li>tabs_selected: The number of tabs added to the collection</li></ul>|2020-09-01 |
| context_menu.item_tapped |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user tapped an item in the browsers context menu |[1](https://github.com/mozilla-mobile/fenix/pull/1344#issuecomment-479285010)|<ul><li>named: The name of the item that was tapped. Available items are: ``` open_in_new_tab, open_in_private_tab, open_image_in_new_tab, save_image, share_link, copy_link, copy_image_location ``` </li></ul>|2020-09-01 |
| contextual_hint.tracking_protection.dismiss |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The enhanced tracking protection contextual hint was dismissed by pressing the close button |[1](https://github.com/mozilla-mobile/fenix/pull/TODO)||2020-09-01 |
| contextual_hint.tracking_protection.display |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The enhanced tracking protection contextual hint was displayed. |[1](https://github.com/mozilla-mobile/fenix/pull/TODO)||2020-09-01 |
| contextual_hint.tracking_protection.inside_tap |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The user tapped inside of the etp contextual hint (which brings up the etp panel for this site). |[1](https://github.com/mozilla-mobile/fenix/pull/TODO)||2020-09-01 |
| contextual_hint.tracking_protection.outside_tap |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The user tapped outside of the etp contextual hint (which has no effect). |[1](https://github.com/mozilla-mobile/fenix/pull/TODO)||2020-09-01 |
| crash_reporter.closed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The crash reporter was closed |[1](https://github.com/mozilla-mobile/fenix/pull/1214#issue-264756708)|<ul><li>crash_submitted: A boolean that tells us whether or not the user submitted a crash report </li></ul>|2020-09-01 |
| crash_reporter.opened |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The crash reporter was displayed |[1](https://github.com/mozilla-mobile/fenix/pull/1214#issue-264756708)||2020-09-01 |
| custom_tab.action_button |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user pressed the action button provided by the launching app |[1](https://github.com/mozilla-mobile/fenix/pull/1697)||2020-09-01 |
@ -122,6 +126,15 @@ The following metrics are added to the ping:
| media_state.pause |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Media playback was paused. |[1](https://github.com/mozilla-mobile/fenix/pull/6463)||2020-09-01 |
| media_state.play |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Media started playing. |[1](https://github.com/mozilla-mobile/fenix/pull/6463)||2020-09-01 |
| media_state.stop |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Media playback has ended. |[1](https://github.com/mozilla-mobile/fenix/pull/6463)||2020-09-01 |
| onboarding.finish |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The user taps starts browsing and ends the onboarding experience. |[1](https://github.com/mozilla-mobile/fenix/pull/11867)||2020-09-01 |
| onboarding.fxa_auto_signin |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The onboarding automatic sign in card was tapped. |[1](https://github.com/mozilla-mobile/fenix/pull/11867)||2020-09-01 |
| onboarding.fxa_manual_signin |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The onboarding manual sign in card was tapped. |[1](https://github.com/mozilla-mobile/fenix/pull/11867)||2020-09-01 |
| onboarding.pref_toggled_private_browsing |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The private browsing preference was selected from the onboarding card. |[1](https://github.com/mozilla-mobile/fenix/pull/11867)||2020-09-01 |
| onboarding.pref_toggled_theme_picker |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The device theme was chosen using the theme picker onboarding card. |[1](https://github.com/mozilla-mobile/fenix/pull/11867)|<ul><li>theme: A string that indicates the theme LIGHT, DARK, or FOLLOW DEVICE. Default: FOLLOW DEVICE </li></ul>|2020-09-01 |
| onboarding.pref_toggled_toolbar_position |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The toolbar position preference was chosen from the onboarding card. |[1](https://github.com/mozilla-mobile/fenix/pull/11867)|<ul><li>position: A string that indicates the position of the toolbar TOP or BOTTOM. Default: BOTTOM </li></ul>|2020-09-01 |
| onboarding.pref_toggled_tracking_prot |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The tracking protection preference was chosen from the onboarding card. |[1](https://github.com/mozilla-mobile/fenix/pull/11867)|<ul><li>position: A string that indicates the Tracking Protection policy STANDARD or STRICT. Default: Toggle ON, STANDARD </li></ul>|2020-09-01 |
| onboarding.privacy_notice |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The onboarding privacy notice card was tapped. |[1](https://github.com/mozilla-mobile/fenix/pull/11867)||2020-09-01 |
| onboarding.whats_new |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The onboarding What\'s New card was tapped. |[1](https://github.com/mozilla-mobile/fenix/pull/11867)||2020-09-01 |
| pocket.pocket_top_site_clicked |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user clicked on the trending Pocket top site |[1](https://github.com/mozilla-mobile/fenix/pull/8098)||2020-09-01 |
| pocket.pocket_top_site_removed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user removed the trending Pocket top site |[1](https://github.com/mozilla-mobile/fenix/pull/8098)||2020-09-01 |
| private_browsing_mode.garbage_icon |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user pressed the garbage can icon on the private browsing home page, deleting all private tabs. |[1](https://github.com/mozilla-mobile/fenix/pull/4968)||2020-09-01 |
@ -188,9 +201,10 @@ The following metrics are added to the ping:
| user_specified_search_engines.custom_engine_deleted |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user deleted a custom search engine |[1](https://github.com/mozilla-mobile/fenix/pull/6918)||2020-09-01 |
| voice_search.tapped |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user selected the voice search button on the search screen. |[1](https://github.com/mozilla-mobile/fenix/pull/10785)||2020-09-01 |
## installation
## first-session
This ping is intended to capture the source of the installation
This ping is intended to capture the source of the app install
on the first session.
This ping includes the [client id](https://mozilla.github.io/glean/book/user/pings/index.html#the-client_info-section).
@ -207,11 +221,11 @@ The following metrics are added to the ping:
| Name | Type | Description | Data reviews | Extras | Expiration |
| --- | --- | --- | --- | --- | --- |
| installation.adgroup |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |The name of the AdGroup that was used to source this installation. |[1](https://github.com/mozilla-mobile/fenix/pull/8074#issuecomment-586480836)||2020-09-01 |
| installation.campaign |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |The name of the campaign that is responsible for this installation. |[1](https://github.com/mozilla-mobile/fenix/pull/8074#issuecomment-586512202)||2020-09-01 |
| installation.creative |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |The identifier of the creative material that the user interacted with. |[1](https://github.com/mozilla-mobile/fenix/pull/8074#issuecomment-586512202)||2020-09-01 |
| installation.network |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |The name of the Network that sourced this installation. |[1](https://github.com/mozilla-mobile/fenix/pull/8074#issuecomment-586512202)||2020-09-01 |
| installation.timestamp |[datetime](https://mozilla.github.io/glean/book/user/metrics/datetime.html) |The Glean generated date and time of the installation. This is unique per app install, though the rest of the data in this ping is from Adjust and will remain static across installs. |[1](https://github.com/mozilla-mobile/fenix/pull/8074#issuecomment-586512202)||2020-09-01 |
| first_session.adgroup |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |The name of the AdGroup that was used to source this installation. |[1](https://github.com/mozilla-mobile/fenix/pull/8074#issuecomment-586480836)||2020-09-01 |
| first_session.campaign |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |The name of the campaign that is responsible for this installation. |[1](https://github.com/mozilla-mobile/fenix/pull/8074#issuecomment-586512202)||2020-09-01 |
| first_session.creative |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |The identifier of the creative material that the user interacted with. |[1](https://github.com/mozilla-mobile/fenix/pull/8074#issuecomment-586512202)||2020-09-01 |
| first_session.network |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |The name of the Network that sourced this installation. |[1](https://github.com/mozilla-mobile/fenix/pull/8074#issuecomment-586512202)||2020-09-01 |
| first_session.timestamp |[datetime](https://mozilla.github.io/glean/book/user/metrics/datetime.html) |The Glean generated date and time of the installation. This is unique per app install, though the rest of the data in this ping is from Adjust and will remain static across installs. |[1](https://github.com/mozilla-mobile/fenix/pull/8074#issuecomment-586512202)||2020-09-01 |
## metrics

View File

@ -41,8 +41,8 @@ jobs:
code-review: true
run:
gradlew: ['clean', '-Pcoverage', 'jacocoGeckoNightlyDebugTestReport']
post-gradlew:
- ['automation/taskcluster/upload_coverage_report.sh']
# post-gradlew:
# - ['automation/taskcluster/upload_coverage_report.sh']
secrets:
- name: project/mobile/fenix/public-tokens
key: codecov

View File

@ -12,7 +12,7 @@ USER worker:worker
ENV GOOGLE_SDK_DOWNLOAD ./gcloud.tar.gz
ENV GOOGLE_SDK_VERSION 233
ENV FLANK_VERSION v20.06.0
ENV FLANK_VERSION v20.06.2
ENV TEST_TOOLS /builds/worker/test-tools
ENV PATH ${PATH}:${TEST_TOOLS}:${TEST_TOOLS}/google-cloud-sdk/bin