Update to new feature-media API.
parent
cd7c2c4bd5
commit
02aabea14a
|
@ -213,6 +213,9 @@
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/Theme.AppCompat.DayNight.DarkActionBar"/>
|
android:theme="@style/Theme.AppCompat.DayNight.DarkActionBar"/>
|
||||||
|
|
||||||
|
<service android:name=".media.MediaService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".customtabs.CustomTabsService"
|
android:name=".customtabs.CustomTabsService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
|
@ -10,20 +10,6 @@ object FeatureFlags {
|
||||||
*/
|
*/
|
||||||
const val pullToRefreshEnabled = false
|
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.
|
* 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.android.synthetic.main.fragment_create_collection.view.*
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import mozilla.components.browser.session.SessionManager
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||||
import mozilla.components.lib.state.ext.consumeFrom
|
import mozilla.components.lib.state.ext.consumeFrom
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
@ -45,10 +46,11 @@ class CollectionCreationFragment : DialogFragment() {
|
||||||
val args: CollectionCreationFragmentArgs by navArgs()
|
val args: CollectionCreationFragmentArgs by navArgs()
|
||||||
|
|
||||||
val sessionManager = requireComponents.core.sessionManager
|
val sessionManager = requireComponents.core.sessionManager
|
||||||
|
val store = requireComponents.core.store
|
||||||
val publicSuffixList = requireComponents.publicSuffixList
|
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) {
|
val selectedTabs = if (args.selectedTabIds != null) {
|
||||||
sessionManager.getTabs(args.selectedTabIds, publicSuffixList).toSet()
|
sessionManager.getTabs(args.selectedTabIds, store, publicSuffixList).toSet()
|
||||||
} else {
|
} else {
|
||||||
if (tabs.size == 1) setOf(tabs.first()) else emptySet()
|
if (tabs.size == 1) setOf(tabs.first()) else emptySet()
|
||||||
}
|
}
|
||||||
|
@ -80,7 +82,10 @@ class CollectionCreationFragment : DialogFragment() {
|
||||||
viewLifecycleOwner.lifecycleScope
|
viewLifecycleOwner.lifecycleScope
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
collectionCreationView = CollectionCreationView(view.createCollectionWrapper, collectionCreationInteractor)
|
collectionCreationView = CollectionCreationView(
|
||||||
|
view.createCollectionWrapper,
|
||||||
|
collectionCreationInteractor
|
||||||
|
)
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
@ -108,9 +113,13 @@ class CollectionCreationFragment : DialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@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
|
return tabIds
|
||||||
?.mapNotNull { this.findSessionById(it) }
|
?.mapNotNull { this.findSessionById(it) }
|
||||||
?.map { it.toTab(publicSuffixList) }
|
?.map { it.toTab(store, publicSuffixList) }
|
||||||
?: emptyList()
|
?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,20 @@ import androidx.transition.AutoTransition
|
||||||
import androidx.transition.Transition
|
import androidx.transition.Transition
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
import kotlinx.android.synthetic.main.component_collection_creation.*
|
import kotlinx.android.synthetic.main.component_collection_creation.back_button
|
||||||
import kotlinx.android.synthetic.main.component_collection_creation.view.*
|
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.feature.tab.collections.TabCollection
|
||||||
import mozilla.components.support.ktx.android.view.hideKeyboard
|
import mozilla.components.support.ktx.android.view.hideKeyboard
|
||||||
import mozilla.components.support.ktx.android.view.showKeyboard
|
import mozilla.components.support.ktx.android.view.showKeyboard
|
||||||
|
@ -253,7 +265,8 @@ class CollectionCreationView(
|
||||||
tab.id.toString(),
|
tab.id.toString(),
|
||||||
tab.url,
|
tab.url,
|
||||||
tab.url.toShortUrl(view.context.components.publicSuffixList),
|
tab.url.toShortUrl(view.context.components.publicSuffixList),
|
||||||
tab.title
|
tab.title,
|
||||||
|
mediaState = MediaState.State.NONE
|
||||||
)
|
)
|
||||||
}.let { tabs ->
|
}.let { tabs ->
|
||||||
collectionCreationTabListAdapter.updateData(tabs, tabs.toSet(), true)
|
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.engine.mediaquery.PreferredColorScheme
|
||||||
import mozilla.components.concept.fetch.Client
|
import mozilla.components.concept.fetch.Client
|
||||||
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
|
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
|
||||||
import mozilla.components.feature.media.MediaFeature
|
|
||||||
import mozilla.components.feature.media.RecordingDevicesNotificationFeature
|
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.ManifestStorage
|
||||||
import mozilla.components.feature.pwa.WebAppShortcutManager
|
import mozilla.components.feature.pwa.WebAppShortcutManager
|
||||||
import mozilla.components.feature.session.HistoryDelegate
|
import mozilla.components.feature.session.HistoryDelegate
|
||||||
|
@ -44,6 +43,7 @@ import org.mozilla.fenix.FeatureFlags
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
|
import org.mozilla.fenix.media.MediaService
|
||||||
import org.mozilla.fenix.test.Mockable
|
import org.mozilla.fenix.test.Mockable
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ class Core(private val context: Context) {
|
||||||
preferredColorScheme = getPreferredColorScheme(),
|
preferredColorScheme = getPreferredColorScheme(),
|
||||||
automaticFontSizeAdjustment = context.settings().shouldUseAutoSize,
|
automaticFontSizeAdjustment = context.settings().shouldUseAutoSize,
|
||||||
fontInflationEnabled = context.settings().shouldUseAutoSize,
|
fontInflationEnabled = context.settings().shouldUseAutoSize,
|
||||||
suspendMediaWhenInactive = !FeatureFlags.mediaIntegration,
|
suspendMediaWhenInactive = false,
|
||||||
forceUserScalableContent = context.settings().forceEnableZoom
|
forceUserScalableContent = context.settings().forceEnableZoom
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -97,7 +97,11 @@ class Core(private val context: Context) {
|
||||||
* The [BrowserStore] holds the global [BrowserState].
|
* The [BrowserStore] holds the global [BrowserState].
|
||||||
*/
|
*/
|
||||||
val store by lazy {
|
val store by lazy {
|
||||||
BrowserStore()
|
BrowserStore(
|
||||||
|
middleware = listOf(
|
||||||
|
MediaMiddleware(context, MediaService::class.java)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -150,14 +154,6 @@ class Core(private val context: Context) {
|
||||||
.whenSessionsChange()
|
.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(
|
WebNotificationFeature(
|
||||||
context, engine, icons, R.drawable.ic_status_logo,
|
context, engine, icons, R.drawable.ic_status_logo,
|
||||||
HomeActivity::class.java
|
HomeActivity::class.java
|
||||||
|
|
|
@ -6,21 +6,36 @@ package org.mozilla.fenix.ext
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import mozilla.components.browser.session.Session
|
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 mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||||
import org.mozilla.fenix.home.Tab
|
import org.mozilla.fenix.home.Tab
|
||||||
|
|
||||||
fun Session.toTab(context: Context, selected: Boolean? = null, mediaState: MediaState? = null): Tab =
|
fun Session.toTab(context: Context, selected: Boolean? = null): Tab =
|
||||||
this.toTab(context.components.publicSuffixList, selected, mediaState)
|
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(
|
return Tab(
|
||||||
sessionId = this.id,
|
sessionId = this.id,
|
||||||
url = this.url,
|
url = this.url,
|
||||||
hostname = this.url.toShortUrl(publicSuffixList),
|
hostname = this.url.toShortUrl(publicSuffixList),
|
||||||
title = this.title,
|
title = this.title,
|
||||||
selected = selected,
|
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.toolbar
|
||||||
import kotlinx.android.synthetic.main.fragment_home.view.toolbarLayout
|
import kotlinx.android.synthetic.main.fragment_home.view.toolbarLayout
|
||||||
import kotlinx.android.synthetic.main.fragment_home.view.toolbar_wrapper
|
import kotlinx.android.synthetic.main.fragment_home.view.toolbar_wrapper
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import mozilla.appservices.places.BookmarkRoot
|
import mozilla.appservices.places.BookmarkRoot
|
||||||
import mozilla.components.browser.menu.ext.getHighlight
|
import mozilla.components.browser.menu.ext.getHighlight
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.session.Session
|
||||||
import mozilla.components.browser.session.SessionManager
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
import mozilla.components.concept.sync.AccountObserver
|
import mozilla.components.concept.sync.AccountObserver
|
||||||
import mozilla.components.concept.sync.AuthType
|
import mozilla.components.concept.sync.AuthType
|
||||||
import mozilla.components.concept.sync.OAuthAccount
|
import mozilla.components.concept.sync.OAuthAccount
|
||||||
import mozilla.components.feature.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.tab.collections.TabCollection
|
||||||
import mozilla.components.feature.top.sites.TopSite
|
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.android.util.dpToPx
|
||||||
|
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
|
||||||
import org.mozilla.fenix.BrowserDirection
|
import org.mozilla.fenix.BrowserDirection
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
@ -108,7 +110,6 @@ import org.mozilla.fenix.whatsnew.WhatsNew
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
private val homeViewModel: HomeScreenViewModel by viewModels {
|
private val homeViewModel: HomeScreenViewModel by viewModels {
|
||||||
|
@ -165,7 +166,11 @@ class HomeFragment : Fragment() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
postponeEnterTransition()
|
postponeEnterTransition()
|
||||||
|
|
||||||
val sessionObserver = BrowserSessionsObserver(sessionManager, singleSessionObserver) {
|
val sessionObserver = BrowserSessionsObserver(
|
||||||
|
sessionManager,
|
||||||
|
requireComponents.core.store,
|
||||||
|
singleSessionObserver
|
||||||
|
) {
|
||||||
emitSessionChanges()
|
emitSessionChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,8 +210,9 @@ class HomeFragment : Fragment() {
|
||||||
|
|
||||||
sessionControlInteractor = SessionControlInteractor(
|
sessionControlInteractor = SessionControlInteractor(
|
||||||
DefaultSessionControlController(
|
DefaultSessionControlController(
|
||||||
|
store = requireComponents.core.store,
|
||||||
activity = activity,
|
activity = activity,
|
||||||
store = homeFragmentStore,
|
fragmentStore = homeFragmentStore,
|
||||||
navController = findNavController(),
|
navController = findNavController(),
|
||||||
browsingModeManager = browsingModeManager,
|
browsingModeManager = browsingModeManager,
|
||||||
lifecycleScope = viewLifecycleOwner.lifecycleScope,
|
lifecycleScope = viewLifecycleOwner.lifecycleScope,
|
||||||
|
@ -277,7 +283,6 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
|
||||||
@SuppressWarnings("LongMethod")
|
@SuppressWarnings("LongMethod")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
@ -895,16 +900,9 @@ class HomeFragment : Fragment() {
|
||||||
|
|
||||||
private fun List<Session>.toTabs(): List<Tab> {
|
private fun List<Session>.toTabs(): List<Tab> {
|
||||||
val selected = sessionManager.selectedSession
|
val selected = sessionManager.selectedSession
|
||||||
val mediaStateSession = MediaStateMachine.state.getSession()
|
|
||||||
|
|
||||||
return this.map {
|
return map {
|
||||||
val mediaState = if (mediaStateSession?.id == it.id) {
|
it.toTab(requireContext(), it == selected)
|
||||||
MediaStateMachine.state
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
it.toTab(requireContext(), it == selected, mediaState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -973,18 +971,24 @@ class HomeFragment : Fragment() {
|
||||||
*/
|
*/
|
||||||
private class BrowserSessionsObserver(
|
private class BrowserSessionsObserver(
|
||||||
private val manager: SessionManager,
|
private val manager: SessionManager,
|
||||||
|
private val store: BrowserStore,
|
||||||
private val observer: Session.Observer,
|
private val observer: Session.Observer,
|
||||||
private val onChanged: () -> Unit
|
private val onChanged: () -> Unit
|
||||||
) : LifecycleObserver {
|
) : LifecycleObserver {
|
||||||
|
private var scope: CoroutineScope? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start observing
|
* Start observing
|
||||||
*/
|
*/
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_START)
|
@OnLifecycleEvent(Lifecycle.Event.ON_START)
|
||||||
fun onStart() {
|
fun onStart() {
|
||||||
MediaStateMachine.register(managerObserver)
|
|
||||||
manager.register(managerObserver)
|
manager.register(managerObserver)
|
||||||
subscribeToAll()
|
subscribeToAll()
|
||||||
|
|
||||||
|
scope = store.flowScoped { flow ->
|
||||||
|
flow.ifChanged { it.media.aggregate }
|
||||||
|
.collect { onChanged() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -992,7 +996,7 @@ private class BrowserSessionsObserver(
|
||||||
*/
|
*/
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||||
fun onStop() {
|
fun onStop() {
|
||||||
MediaStateMachine.unregister(managerObserver)
|
scope?.cancel()
|
||||||
manager.unregister(managerObserver)
|
manager.unregister(managerObserver)
|
||||||
unsubscribeFromAll()
|
unsubscribeFromAll()
|
||||||
}
|
}
|
||||||
|
@ -1013,11 +1017,7 @@ private class BrowserSessionsObserver(
|
||||||
session.unregister(observer)
|
session.unregister(observer)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val managerObserver = object : SessionManager.Observer, MediaStateMachine.Observer {
|
private val managerObserver = object : SessionManager.Observer {
|
||||||
override fun onStateChanged(state: MediaState) {
|
|
||||||
onChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSessionAdded(session: Session) {
|
override fun onSessionAdded(session: Session) {
|
||||||
subscribeTo(session)
|
subscribeTo(session)
|
||||||
onChanged()
|
onChanged()
|
||||||
|
|
|
@ -7,7 +7,7 @@ package org.mozilla.fenix.home
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.session.Session
|
||||||
import mozilla.components.browser.session.SessionManager
|
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.tab.collections.TabCollection
|
||||||
import mozilla.components.feature.top.sites.TopSite
|
import mozilla.components.feature.top.sites.TopSite
|
||||||
import mozilla.components.lib.state.Action
|
import mozilla.components.lib.state.Action
|
||||||
|
@ -29,8 +29,8 @@ data class Tab(
|
||||||
val hostname: String,
|
val hostname: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val selected: Boolean? = null,
|
val selected: Boolean? = null,
|
||||||
var mediaState: MediaState? = null,
|
val icon: Bitmap? = null,
|
||||||
val icon: Bitmap? = null
|
val mediaState: MediaState.State
|
||||||
)
|
)
|
||||||
|
|
||||||
data class TopSiteItem(
|
data class TopSiteItem(
|
||||||
|
|
|
@ -15,7 +15,6 @@ import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.android.synthetic.main.tab_list_row.*
|
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.tab.collections.TabCollection
|
||||||
import mozilla.components.feature.top.sites.TopSite
|
import mozilla.components.feature.top.sites.TopSite
|
||||||
import org.mozilla.fenix.home.OnboardingState
|
import org.mozilla.fenix.home.OnboardingState
|
||||||
|
@ -256,7 +255,7 @@ class SessionControlAdapter(
|
||||||
}
|
}
|
||||||
if (it.shouldUpdateSelected) { holder.updateSelected(it.tab.selected ?: false) }
|
if (it.shouldUpdateSelected) { holder.updateSelected(it.tab.selected ?: false) }
|
||||||
if (it.shouldUpdateMediaState) {
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mozilla.components.browser.session.SessionManager
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
import mozilla.components.concept.engine.prompt.ShareData
|
import mozilla.components.concept.engine.prompt.ShareData
|
||||||
import mozilla.components.feature.media.ext.pauseIfPlaying
|
import mozilla.components.feature.media.ext.pauseIfPlaying
|
||||||
import mozilla.components.feature.media.ext.playIfPaused
|
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.TabCollection
|
||||||
import mozilla.components.feature.tab.collections.ext.restore
|
import mozilla.components.feature.tab.collections.ext.restore
|
||||||
import mozilla.components.feature.top.sites.TopSite
|
import mozilla.components.feature.top.sites.TopSite
|
||||||
|
@ -157,8 +157,9 @@ interface SessionControlController {
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
class DefaultSessionControlController(
|
class DefaultSessionControlController(
|
||||||
|
private val store: BrowserStore,
|
||||||
private val activity: HomeActivity,
|
private val activity: HomeActivity,
|
||||||
private val store: HomeFragmentStore,
|
private val fragmentStore: HomeFragmentStore,
|
||||||
private val navController: NavController,
|
private val navController: NavController,
|
||||||
private val browsingModeManager: BrowsingModeManager,
|
private val browsingModeManager: BrowsingModeManager,
|
||||||
private val lifecycleScope: CoroutineScope,
|
private val lifecycleScope: CoroutineScope,
|
||||||
|
@ -266,11 +267,11 @@ class DefaultSessionControlController(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handlePauseMediaClicked() {
|
override fun handlePauseMediaClicked() {
|
||||||
MediaStateMachine.state.pauseIfPlaying()
|
store.state.media.pauseIfPlaying()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handlePlayMediaClicked() {
|
override fun handlePlayMediaClicked() {
|
||||||
MediaStateMachine.state.playIfPaused()
|
store.state.media.playIfPaused()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handlePrivateBrowsingLearnMoreClicked() {
|
override fun handlePrivateBrowsingLearnMoreClicked() {
|
||||||
|
@ -358,7 +359,7 @@ class DefaultSessionControlController(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean) {
|
override fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean) {
|
||||||
store.dispatch(HomeFragmentAction.CollectionExpanded(collection, expand))
|
fragmentStore.dispatch(HomeFragmentAction.CollectionExpanded(collection, expand))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleonOpenNewTabClicked() {
|
override fun handleonOpenNewTabClicked() {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
import kotlinx.android.synthetic.main.tab_list_row.*
|
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 mozilla.components.support.ktx.android.util.dpToFloat
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
@ -50,15 +50,19 @@ class TabViewHolder(
|
||||||
|
|
||||||
play_pause_button.setOnClickListener {
|
play_pause_button.setOnClickListener {
|
||||||
when (tab?.mediaState) {
|
when (tab?.mediaState) {
|
||||||
is MediaState.Playing -> {
|
MediaState.State.PLAYING -> {
|
||||||
it.context.components.analytics.metrics.track(Event.TabMediaPause)
|
it.context.components.analytics.metrics.track(Event.TabMediaPause)
|
||||||
interactor.onPauseMediaClicked()
|
interactor.onPauseMediaClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
is MediaState.Paused -> {
|
MediaState.State.PAUSED -> {
|
||||||
it.context.components.analytics.metrics.track(Event.TabMediaPlay)
|
it.context.components.analytics.metrics.track(Event.TabMediaPlay)
|
||||||
interactor.onPlayMediaClicked()
|
interactor.onPlayMediaClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MediaState.State.NONE -> throw AssertionError(
|
||||||
|
"Play/Pause button clicked without play/pause state."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,20 +86,20 @@ class TabViewHolder(
|
||||||
updateHostname(tab.hostname)
|
updateHostname(tab.hostname)
|
||||||
updateFavIcon(tab.url, tab.icon)
|
updateFavIcon(tab.url, tab.icon)
|
||||||
updateSelected(tab.selected ?: false)
|
updateSelected(tab.selected ?: false)
|
||||||
updatePlayPauseButton(tab.mediaState ?: MediaState.None)
|
updatePlayPauseButton(tab.mediaState)
|
||||||
item_tab.transitionName = "$TAB_ITEM_TRANSITION_NAME${tab.sessionId}"
|
item_tab.transitionName = "$TAB_ITEM_TRANSITION_NAME${tab.sessionId}"
|
||||||
updateCloseButtonDescription(tab.title)
|
updateCloseButtonDescription(tab.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun updatePlayPauseButton(mediaState: MediaState) {
|
internal fun updatePlayPauseButton(mediaState: MediaState.State) {
|
||||||
with(play_pause_button) {
|
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
|
View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaState is MediaState.Playing) {
|
if (mediaState == MediaState.State.PLAYING) {
|
||||||
play_pause_button.contentDescription =
|
play_pause_button.contentDescription =
|
||||||
context.getString(R.string.mozac_feature_media_notification_action_pause)
|
context.getString(R.string.mozac_feature_media_notification_action_pause)
|
||||||
setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.pause_with_background))
|
setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.pause_with_background))
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/* 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 kotlinx.coroutines.async
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.session.Session
|
||||||
import mozilla.components.browser.session.SessionManager
|
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.feature.tab.collections.Tab
|
||||||
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
@ -44,6 +46,7 @@ class CollectionCreationFragmentTest {
|
||||||
|
|
||||||
@MockK private lateinit var sessionManager: SessionManager
|
@MockK private lateinit var sessionManager: SessionManager
|
||||||
@MockK private lateinit var publicSuffixList: PublicSuffixList
|
@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 sessionMozilla = Session(initialUrl = URL_MOZILLA, id = SESSION_ID_MOZILLA)
|
||||||
private val sessionBcc = Session(initialUrl = URL_BCC, id = SESSION_ID_BCC)
|
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 { sessionManager.findSessionById(SESSION_ID_BAD_2) } answers { null }
|
||||||
every { publicSuffixList.stripPublicSuffix(URL_MOZILLA) } answers { GlobalScope.async { URL_MOZILLA } }
|
every { publicSuffixList.stripPublicSuffix(URL_MOZILLA) } answers { GlobalScope.async { URL_MOZILLA } }
|
||||||
every { publicSuffixList.stripPublicSuffix(URL_BCC) } answers { GlobalScope.async { URL_BCC } }
|
every { publicSuffixList.stripPublicSuffix(URL_BCC) } answers { GlobalScope.async { URL_BCC } }
|
||||||
|
every { store.state } answers { BrowserState() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -80,7 +84,7 @@ class CollectionCreationFragmentTest {
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN tabs are present in session manager WHEN getTabs is called THEN tabs will be returned`() {
|
fun `GIVEN tabs are present in session manager WHEN getTabs is called THEN tabs will be returned`() {
|
||||||
val tabs = sessionManager
|
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 }
|
val hosts = tabs.map { it.hostname }
|
||||||
|
|
||||||
|
@ -91,7 +95,7 @@ class CollectionCreationFragmentTest {
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN some tabs are present in session manager WHEN getTabs is called THEN only valid tabs will be returned`() {
|
fun `GIVEN some tabs are present in session manager WHEN getTabs is called THEN only valid tabs will be returned`() {
|
||||||
val tabs = sessionManager
|
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 }
|
val hosts = tabs.map { it.hostname }
|
||||||
|
|
||||||
|
@ -102,7 +106,7 @@ class CollectionCreationFragmentTest {
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN tabs are not present in session manager WHEN getTabs is called THEN an empty list will be returned`() {
|
fun `GIVEN tabs are not present in session manager WHEN getTabs is called THEN an empty list will be returned`() {
|
||||||
val tabs = sessionManager
|
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)
|
assertEquals(emptyList<Tab>(), tabs)
|
||||||
}
|
}
|
||||||
|
@ -110,7 +114,7 @@ class CollectionCreationFragmentTest {
|
||||||
@Test
|
@Test
|
||||||
fun `WHEN getTabs is called will null tabIds THEN an empty list will be returned`() {
|
fun `WHEN getTabs is called will null tabIds THEN an empty list will be returned`() {
|
||||||
val tabs = sessionManager
|
val tabs = sessionManager
|
||||||
.getTabs(null, publicSuffixList)
|
.getTabs(null, store, publicSuffixList)
|
||||||
|
|
||||||
assertEquals(emptyList<Tab>(), tabs)
|
assertEquals(emptyList<Tab>(), tabs)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import kotlinx.coroutines.newSingleThreadContext
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
import mozilla.components.browser.session.SessionManager
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
import mozilla.components.concept.engine.Engine
|
import mozilla.components.concept.engine.Engine
|
||||||
import mozilla.components.feature.tab.collections.TabCollection
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import mozilla.components.feature.tabs.TabsUseCases
|
import mozilla.components.feature.tabs.TabsUseCases
|
||||||
|
@ -38,8 +39,9 @@ import mozilla.components.feature.tab.collections.Tab as ComponentTab
|
||||||
class DefaultSessionControlControllerTest {
|
class DefaultSessionControlControllerTest {
|
||||||
|
|
||||||
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
|
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
|
||||||
|
private val store: BrowserStore = mockk(relaxed = true)
|
||||||
private val activity: HomeActivity = 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 navController: NavController = mockk(relaxed = true)
|
||||||
private val browsingModeManager: BrowsingModeManager = mockk(relaxed = true)
|
private val browsingModeManager: BrowsingModeManager = mockk(relaxed = true)
|
||||||
private val closeTab: (sessionId: String) -> Unit = mockk(relaxed = true)
|
private val closeTab: (sessionId: String) -> Unit = mockk(relaxed = true)
|
||||||
|
@ -71,7 +73,7 @@ class DefaultSessionControlControllerTest {
|
||||||
every { activity.components.core.tabCollectionStorage } returns tabCollectionStorage
|
every { activity.components.core.tabCollectionStorage } returns tabCollectionStorage
|
||||||
every { activity.components.useCases.tabsUseCases } returns tabsUseCases
|
every { activity.components.useCases.tabsUseCases } returns tabsUseCases
|
||||||
|
|
||||||
every { store.state } returns state
|
every { fragmentStore.state } returns state
|
||||||
every { state.collections } returns emptyList()
|
every { state.collections } returns emptyList()
|
||||||
every { state.expandedCollections } returns emptySet()
|
every { state.expandedCollections } returns emptySet()
|
||||||
every { state.mode } returns Mode.Normal
|
every { state.mode } returns Mode.Normal
|
||||||
|
@ -79,8 +81,9 @@ class DefaultSessionControlControllerTest {
|
||||||
every { activity.components.analytics.metrics } returns metrics
|
every { activity.components.analytics.metrics } returns metrics
|
||||||
|
|
||||||
controller = DefaultSessionControlController(
|
controller = DefaultSessionControlController(
|
||||||
activity = activity,
|
|
||||||
store = store,
|
store = store,
|
||||||
|
activity = activity,
|
||||||
|
fragmentStore = fragmentStore,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
browsingModeManager = browsingModeManager,
|
browsingModeManager = browsingModeManager,
|
||||||
lifecycleScope = MainScope(),
|
lifecycleScope = MainScope(),
|
||||||
|
@ -235,6 +238,6 @@ class DefaultSessionControlControllerTest {
|
||||||
fun handleToggleCollectionExpanded() {
|
fun handleToggleCollectionExpanded() {
|
||||||
val collection: TabCollection = mockk(relaxed = true)
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
controller.handleToggleCollectionExpanded(collection, true)
|
controller.handleToggleCollectionExpanded(collection, true)
|
||||||
verify { store.dispatch(HomeFragmentAction.CollectionExpanded(collection, true)) }
|
verify { fragmentStore.dispatch(HomeFragmentAction.CollectionExpanded(collection, true)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,5 +3,5 @@
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
object AndroidComponents {
|
object AndroidComponents {
|
||||||
const val VERSION = "38.0.20200326160141"
|
const val VERSION = "38.0.20200327101347"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue