Copione merged onto master
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
commit
a8e16ed074
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
188
app/metrics.yaml
188
app/metrics.yaml
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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.")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() &&
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -126,7 +126,6 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
|||
view.context.components.core.store,
|
||||
selectTabUseCase,
|
||||
removeTabUseCase,
|
||||
view.context.components.useCases.thumbnailUseCases,
|
||||
{ it.content.private == isPrivate },
|
||||
{ }
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -887,8 +887,6 @@
|
|||
<string name="preference_summary_delete_browsing_data_on_quit">جب آپ مین مینو سے "چھوڑیں" کو منتخب کرتے ہیں تو براؤزنگ کا ڈیٹا خودبخود حذف ہو جاتا ہے</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">جب آپ مین مینو سے \"چھوڑیں\" کو منتخب کرتے ہیں تو براؤزنگ کا ڈیٹا خودبخود حذف ہو جاتا ہے</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 -->
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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() }
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue