Commit 02aabea1 authored by Sebastian Kaspari's avatar Sebastian Kaspari Committed by Emily Kager
Browse files

Update to new feature-media API.

parent cd7c2c4b
......@@ -213,6 +213,9 @@
android:exported="false"
android:theme="@style/Theme.AppCompat.DayNight.DarkActionBar"/>
<service android:name=".media.MediaService"
android:exported="false" />
<service
android:name=".customtabs.CustomTabsService"
android:exported="true"
......
......@@ -10,20 +10,6 @@ object FeatureFlags {
*/
const val pullToRefreshEnabled = false
/**
* Integration of media features provided by `feature-media` component:
* - Background playback without the app getting killed
* - Media notification with play/pause controls
* - Audio Focus handling (pausing/resuming in agreement with other media apps)
* - Support for hardware controls to toggle play/pause (e.g. buttons on a headset)
*
* Behind nightly flag until all related Android Components issues are fixed and QA has signed
* off.
*
* https://github.com/mozilla-mobile/fenix/issues/4431
*/
const val mediaIntegration = true
/**
* Allows Progressive Web Apps to be installed to the device home screen.
*/
......
......@@ -16,6 +16,7 @@ import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_create_collection.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.R
......@@ -45,10 +46,11 @@ class CollectionCreationFragment : DialogFragment() {
val args: CollectionCreationFragmentArgs by navArgs()
val sessionManager = requireComponents.core.sessionManager
val store = requireComponents.core.store
val publicSuffixList = requireComponents.publicSuffixList
val tabs = sessionManager.getTabs(args.tabIds, publicSuffixList)
val tabs = sessionManager.getTabs(args.tabIds, store, publicSuffixList)
val selectedTabs = if (args.selectedTabIds != null) {
sessionManager.getTabs(args.selectedTabIds, publicSuffixList).toSet()
sessionManager.getTabs(args.selectedTabIds, store, publicSuffixList).toSet()
} else {
if (tabs.size == 1) setOf(tabs.first()) else emptySet()
}
......@@ -80,7 +82,10 @@ class CollectionCreationFragment : DialogFragment() {
viewLifecycleOwner.lifecycleScope
)
)
collectionCreationView = CollectionCreationView(view.createCollectionWrapper, collectionCreationInteractor)
collectionCreationView = CollectionCreationView(
view.createCollectionWrapper,
collectionCreationInteractor
)
return view
}
......@@ -108,9 +113,13 @@ class CollectionCreationFragment : DialogFragment() {
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun SessionManager.getTabs(tabIds: Array<String>?, publicSuffixList: PublicSuffixList): List<Tab> {
fun SessionManager.getTabs(
tabIds: Array<String>?,
store: BrowserStore,
publicSuffixList: PublicSuffixList
): List<Tab> {
return tabIds
?.mapNotNull { this.findSessionById(it) }
?.map { it.toTab(publicSuffixList) }
?.map { it.toTab(store, publicSuffixList) }
?: emptyList()
}
......@@ -20,8 +20,20 @@ import androidx.transition.AutoTransition
import androidx.transition.Transition
import androidx.transition.TransitionManager
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_collection_creation.*
import kotlinx.android.synthetic.main.component_collection_creation.view.*
import kotlinx.android.synthetic.main.component_collection_creation.back_button
import kotlinx.android.synthetic.main.component_collection_creation.collection_constraint_layout
import kotlinx.android.synthetic.main.component_collection_creation.name_collection_edittext
import kotlinx.android.synthetic.main.component_collection_creation.save_button
import kotlinx.android.synthetic.main.component_collection_creation.select_all_button
import kotlinx.android.synthetic.main.component_collection_creation.view.bottom_bar_icon_button
import kotlinx.android.synthetic.main.component_collection_creation.view.bottom_bar_text
import kotlinx.android.synthetic.main.component_collection_creation.view.bottom_button_bar_layout
import kotlinx.android.synthetic.main.component_collection_creation.view.collection_constraint_layout
import kotlinx.android.synthetic.main.component_collection_creation.view.collections_list
import kotlinx.android.synthetic.main.component_collection_creation.view.name_collection_edittext
import kotlinx.android.synthetic.main.component_collection_creation.view.select_all_button
import kotlinx.android.synthetic.main.component_collection_creation.view.tab_list
import mozilla.components.browser.state.state.MediaState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.android.view.showKeyboard
......@@ -253,7 +265,8 @@ class CollectionCreationView(
tab.id.toString(),
tab.url,
tab.url.toShortUrl(view.context.components.publicSuffixList),
tab.title
tab.title,
mediaState = MediaState.State.NONE
)
}.let { tabs ->
collectionCreationTabListAdapter.updateData(tabs, tabs.toSet(), true)
......
......@@ -27,9 +27,8 @@ import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.fetch.Client
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
import mozilla.components.feature.media.MediaFeature
import mozilla.components.feature.media.RecordingDevicesNotificationFeature
import mozilla.components.feature.media.state.MediaStateMachine
import mozilla.components.feature.media.middleware.MediaMiddleware
import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.WebAppShortcutManager
import mozilla.components.feature.session.HistoryDelegate
......@@ -44,6 +43,7 @@ import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.media.MediaService
import org.mozilla.fenix.test.Mockable
import java.util.concurrent.TimeUnit
......@@ -66,7 +66,7 @@ class Core(private val context: Context) {
preferredColorScheme = getPreferredColorScheme(),
automaticFontSizeAdjustment = context.settings().shouldUseAutoSize,
fontInflationEnabled = context.settings().shouldUseAutoSize,
suspendMediaWhenInactive = !FeatureFlags.mediaIntegration,
suspendMediaWhenInactive = false,
forceUserScalableContent = context.settings().forceEnableZoom
)
......@@ -97,7 +97,11 @@ class Core(private val context: Context) {
* The [BrowserStore] holds the global [BrowserState].
*/
val store by lazy {
BrowserStore()
BrowserStore(
middleware = listOf(
MediaMiddleware(context, MediaService::class.java)
)
)
}
/**
......@@ -150,14 +154,6 @@ class Core(private val context: Context) {
.whenSessionsChange()
}
if (FeatureFlags.mediaIntegration) {
MediaStateMachine.start(sessionManager)
// Enable media features like showing an ongoing notification with media controls when
// media in web content is playing.
MediaFeature(context).enable()
}
WebNotificationFeature(
context, engine, icons, R.drawable.ic_status_logo,
HomeActivity::class.java
......
......@@ -6,21 +6,36 @@ package org.mozilla.fenix.ext
import android.content.Context
import mozilla.components.browser.session.Session
import mozilla.components.feature.media.state.MediaState
import mozilla.components.browser.state.state.MediaState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import org.mozilla.fenix.home.Tab
fun Session.toTab(context: Context, selected: Boolean? = null, mediaState: MediaState? = null): Tab =
this.toTab(context.components.publicSuffixList, selected, mediaState)
fun Session.toTab(context: Context, selected: Boolean? = null): Tab =
this.toTab(
context.components.core.store,
context.components.publicSuffixList,
selected
)
fun Session.toTab(publicSuffixList: PublicSuffixList, selected: Boolean? = null, mediaState: MediaState? = null): Tab {
fun Session.toTab(store: BrowserStore, publicSuffixList: PublicSuffixList, selected: Boolean? = null): Tab {
return Tab(
sessionId = this.id,
url = this.url,
hostname = this.url.toShortUrl(publicSuffixList),
title = this.title,
selected = selected,
mediaState = mediaState,
icon = this.icon
icon = this.icon,
mediaState = getMediaStateForSession(store, this)
)
}
private fun getMediaStateForSession(store: BrowserStore, session: Session): MediaState.State {
// For now we are looking up the media state for this session in the BrowserStore. Eventually
// we will migrate away from Session(Manager) and can use BrowserStore and BrowserState directly.
return if (store.state.media.aggregate.activeTabId == session.id) {
store.state.media.aggregate.state
} else {
MediaState.State.NONE
}
}
......@@ -56,25 +56,27 @@ import kotlinx.android.synthetic.main.fragment_home.view.sessionControlRecyclerV
import kotlinx.android.synthetic.main.fragment_home.view.toolbar
import kotlinx.android.synthetic.main.fragment_home.view.toolbarLayout
import kotlinx.android.synthetic.main.fragment_home.view.toolbar_wrapper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.menu.ext.getHighlight
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.feature.media.ext.getSession
import mozilla.components.feature.media.state.MediaState
import mozilla.components.feature.media.state.MediaStateMachine
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.android.util.dpToPx
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
......@@ -108,7 +110,6 @@ import org.mozilla.fenix.whatsnew.WhatsNew
import kotlin.math.abs
import kotlin.math.min
@ExperimentalCoroutinesApi
@SuppressWarnings("TooManyFunctions", "LargeClass")
class HomeFragment : Fragment() {
private val homeViewModel: HomeScreenViewModel by viewModels {
......@@ -165,7 +166,11 @@ class HomeFragment : Fragment() {
super.onCreate(savedInstanceState)
postponeEnterTransition()
val sessionObserver = BrowserSessionsObserver(sessionManager, singleSessionObserver) {
val sessionObserver = BrowserSessionsObserver(
sessionManager,
requireComponents.core.store,
singleSessionObserver
) {
emitSessionChanges()
}
......@@ -205,8 +210,9 @@ class HomeFragment : Fragment() {
sessionControlInteractor = SessionControlInteractor(
DefaultSessionControlController(
store = requireComponents.core.store,
activity = activity,
store = homeFragmentStore,
fragmentStore = homeFragmentStore,
navController = findNavController(),
browsingModeManager = browsingModeManager,
lifecycleScope = viewLifecycleOwner.lifecycleScope,
......@@ -277,7 +283,6 @@ class HomeFragment : Fragment() {
}
}
@ExperimentalCoroutinesApi
@SuppressWarnings("LongMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
......@@ -895,16 +900,9 @@ class HomeFragment : Fragment() {
private fun List<Session>.toTabs(): List<Tab> {
val selected = sessionManager.selectedSession
val mediaStateSession = MediaStateMachine.state.getSession()
return this.map {
val mediaState = if (mediaStateSession?.id == it.id) {
MediaStateMachine.state
} else {
null
}
it.toTab(requireContext(), it == selected, mediaState)
return map {
it.toTab(requireContext(), it == selected)
}
}
......@@ -973,18 +971,24 @@ class HomeFragment : Fragment() {
*/
private class BrowserSessionsObserver(
private val manager: SessionManager,
private val store: BrowserStore,
private val observer: Session.Observer,
private val onChanged: () -> Unit
) : LifecycleObserver {
private var scope: CoroutineScope? = null
/**
* Start observing
*/
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() {
MediaStateMachine.register(managerObserver)
manager.register(managerObserver)
subscribeToAll()
scope = store.flowScoped { flow ->
flow.ifChanged { it.media.aggregate }
.collect { onChanged() }
}
}
/**
......@@ -992,7 +996,7 @@ private class BrowserSessionsObserver(
*/
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
MediaStateMachine.unregister(managerObserver)
scope?.cancel()
manager.unregister(managerObserver)
unsubscribeFromAll()
}
......@@ -1013,11 +1017,7 @@ private class BrowserSessionsObserver(
session.unregister(observer)
}
private val managerObserver = object : SessionManager.Observer, MediaStateMachine.Observer {
override fun onStateChanged(state: MediaState) {
onChanged()
}
private val managerObserver = object : SessionManager.Observer {
override fun onSessionAdded(session: Session) {
subscribeTo(session)
onChanged()
......
......@@ -7,7 +7,7 @@ package org.mozilla.fenix.home
import android.graphics.Bitmap
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.feature.media.state.MediaState
import mozilla.components.browser.state.state.MediaState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.lib.state.Action
......@@ -29,8 +29,8 @@ data class Tab(
val hostname: String,
val title: String,
val selected: Boolean? = null,
var mediaState: MediaState? = null,
val icon: Bitmap? = null
val icon: Bitmap? = null,
val mediaState: MediaState.State
)
data class TopSiteItem(
......
......@@ -15,7 +15,6 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.tab_list_row.*
import mozilla.components.feature.media.state.MediaState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.home.OnboardingState
......@@ -256,7 +255,7 @@ class SessionControlAdapter(
}
if (it.shouldUpdateSelected) { holder.updateSelected(it.tab.selected ?: false) }
if (it.shouldUpdateMediaState) {
holder.updatePlayPauseButton(it.tab.mediaState ?: MediaState.None)
holder.updatePlayPauseButton(it.tab.mediaState)
}
}
}
......
......@@ -10,10 +10,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.media.ext.pauseIfPlaying
import mozilla.components.feature.media.ext.playIfPaused
import mozilla.components.feature.media.state.MediaStateMachine
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tab.collections.ext.restore
import mozilla.components.feature.top.sites.TopSite
......@@ -157,8 +157,9 @@ interface SessionControlController {
@SuppressWarnings("TooManyFunctions", "LargeClass")
class DefaultSessionControlController(
private val store: BrowserStore,
private val activity: HomeActivity,
private val store: HomeFragmentStore,
private val fragmentStore: HomeFragmentStore,
private val navController: NavController,
private val browsingModeManager: BrowsingModeManager,
private val lifecycleScope: CoroutineScope,
......@@ -266,11 +267,11 @@ class DefaultSessionControlController(
}
override fun handlePauseMediaClicked() {
MediaStateMachine.state.pauseIfPlaying()
store.state.media.pauseIfPlaying()
}
override fun handlePlayMediaClicked() {
MediaStateMachine.state.playIfPaused()
store.state.media.playIfPaused()
}
override fun handlePrivateBrowsingLearnMoreClicked() {
......@@ -358,7 +359,7 @@ class DefaultSessionControlController(
}
override fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean) {
store.dispatch(HomeFragmentAction.CollectionExpanded(collection, expand))
fragmentStore.dispatch(HomeFragmentAction.CollectionExpanded(collection, expand))
}
override fun handleonOpenNewTabClicked() {
......
......@@ -12,7 +12,7 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.tab_list_row.*
import mozilla.components.feature.media.state.MediaState
import mozilla.components.browser.state.state.MediaState
import mozilla.components.support.ktx.android.util.dpToFloat
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
......@@ -50,15 +50,19 @@ class TabViewHolder(
play_pause_button.setOnClickListener {
when (tab?.mediaState) {
is MediaState.Playing -> {
MediaState.State.PLAYING -> {
it.context.components.analytics.metrics.track(Event.TabMediaPause)
interactor.onPauseMediaClicked()
}
is MediaState.Paused -> {
MediaState.State.PAUSED -> {
it.context.components.analytics.metrics.track(Event.TabMediaPlay)
interactor.onPlayMediaClicked()
}
MediaState.State.NONE -> throw AssertionError(
"Play/Pause button clicked without play/pause state."
)
}
}
......@@ -82,20 +86,20 @@ class TabViewHolder(
updateHostname(tab.hostname)
updateFavIcon(tab.url, tab.icon)
updateSelected(tab.selected ?: false)
updatePlayPauseButton(tab.mediaState ?: MediaState.None)
updatePlayPauseButton(tab.mediaState)
item_tab.transitionName = "$TAB_ITEM_TRANSITION_NAME${tab.sessionId}"
updateCloseButtonDescription(tab.title)
}
internal fun updatePlayPauseButton(mediaState: MediaState) {
internal fun updatePlayPauseButton(mediaState: MediaState.State) {
with(play_pause_button) {
visibility = if (mediaState is MediaState.Playing || mediaState is MediaState.Paused) {
visibility = if (mediaState == MediaState.State.PLAYING || mediaState == MediaState.State.PAUSED) {
View.VISIBLE
} else {
View.GONE
}
if (mediaState is MediaState.Playing) {
if (mediaState == MediaState.State.PLAYING) {
play_pause_button.contentDescription =
context.getString(R.string.mozac_feature_media_notification_action_pause)
setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.pause_with_background))
......
/* 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.media
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.media.service.AbstractMediaService
import org.mozilla.fenix.ext.components
/**
* [AbstractMediaService] implementation for injecting [BrowserStore] singleton.
*/
class MediaService : AbstractMediaService() {
override val store: BrowserStore by lazy { components.core.store }
}
......@@ -19,6 +19,8 @@ import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.async
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.tab.collections.Tab
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import org.junit.Assert.assertEquals
......@@ -44,6 +46,7 @@ class CollectionCreationFragmentTest {
@MockK private lateinit var sessionManager: SessionManager
@MockK private lateinit var publicSuffixList: PublicSuffixList
@MockK private lateinit var store: BrowserStore
private val sessionMozilla = Session(initialUrl = URL_MOZILLA, id = SESSION_ID_MOZILLA)
private val sessionBcc = Session(initialUrl = URL_BCC, id = SESSION_ID_BCC)
......@@ -57,6 +60,7 @@ class CollectionCreationFragmentTest {
every { sessionManager.findSessionById(SESSION_ID_BAD_2) } answers { null }
every { publicSuffixList.stripPublicSuffix(URL_MOZILLA) } answers { GlobalScope.async { URL_MOZILLA } }
every { publicSuffixList.stripPublicSuffix(URL_BCC) } answers { GlobalScope.async { URL_BCC } }
every { store.state } answers { BrowserState() }
}
@Test
......@@ -80,7 +84,7 @@ class CollectionCreationFragmentTest {
@Test
fun `GIVEN tabs are present in session manager WHEN getTabs is called THEN tabs will be returned`() {
val tabs = sessionManager
.getTabs(arrayOf(SESSION_ID_MOZILLA, SESSION_ID_BCC), publicSuffixList)
.getTabs(arrayOf(SESSION_ID_MOZILLA, SESSION_ID_BCC), store, publicSuffixList)
val hosts = tabs.map { it.hostname }
......@@ -91,7 +95,7 @@ class CollectionCreationFragmentTest {
@Test
fun `GIVEN some tabs are present in session manager WHEN getTabs is called THEN only valid tabs will be returned`() {
val tabs = sessionManager
.getTabs(arrayOf(SESSION_ID_MOZILLA, SESSION_ID_BAD_1), publicSuffixList)
.getTabs(arrayOf(SESSION_ID_MOZILLA, SESSION_ID_BAD_1), store, publicSuffixList)
val hosts = tabs.map { it.hostname }
......@@ -102,7 +106,7 @@ class CollectionCreationFragmentTest {
@Test
fun `GIVEN tabs are not present in session manager WHEN getTabs is called THEN an empty list will be returned`() {
val tabs = sessionManager
.getTabs(arrayOf(SESSION_ID_BAD_1, SESSION_ID_BAD_2), publicSuffixList)
.getTabs(arrayOf(SESSION_ID_BAD_1, SESSION_ID_BAD_2), store, publicSuffixList)
assertEquals(emptyList<Tab>(), tabs)
}
......@@ -110,7 +114,7 @@ class CollectionCreationFragmentTest {
@Test
fun `WHEN getTabs is called will null tabIds THEN an empty list will be returned`() {
val tabs = sessionManager
.getTabs(null, publicSuffixList)
.getTabs(null, store, publicSuffixList)
assertEquals(emptyList<Tab>(), tabs)
}
......
......@@ -17,6 +17,7 @@ import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.TabsUseCases
......@@ -38,8 +39,9 @@ import mozilla.components.feature.tab.collections.Tab as ComponentTab
class DefaultSessionControlControllerTest {
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
private val store: BrowserStore = mockk(relaxed = true)
private val activity: HomeActivity = mockk(relaxed = true)
private val store: HomeFragmentStore = mockk(relaxed = true)
private val fragmentStore: HomeFragmentStore = mockk(relaxed = true)
private val navController: NavController = mockk(relaxed = true)
private val browsingModeManager: BrowsingModeManager = mockk(relaxed = true)
private val closeTab: (sessionId: String) -> Unit = mockk(relaxed = true)