Closes #16896: Integrate new MediaSession API to nightly or debug builds (#16909)

upstream-sync
Roger Yang 4 years ago committed by GitHub
parent 80ad6dea32
commit 2ff7ba75c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -232,6 +232,9 @@
<service android:name=".media.MediaService"
android:exported="false" />
<service android:name=".media.MediaSessionService"
android:exported="false" />
<service
android:name=".customtabs.CustomTabsService"
android:exported="true"

@ -36,4 +36,9 @@ object FeatureFlags {
* all experiments.
*/
val nimbusExperiments = Config.channel.isNightlyOrDebug
/**
* Enables the new MediaSession API.
*/
val newMediaSessionApi = Config.channel.isNightlyOrDebug
}

@ -58,7 +58,7 @@ import mozilla.components.feature.contextmenu.ContextMenuFeature
import mozilla.components.feature.downloads.DownloadsFeature
import mozilla.components.feature.downloads.manager.FetchDownloadManager
import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID
import mozilla.components.feature.media.fullscreen.MediaFullscreenOrientationFeature
import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature
import mozilla.components.feature.privatemode.feature.SecureWindowFeature
import mozilla.components.feature.prompts.PromptFeature
import mozilla.components.feature.prompts.share.ShareDelegate
@ -123,6 +123,8 @@ import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
import java.lang.ref.WeakReference
import mozilla.components.feature.media.fullscreen.MediaFullscreenOrientationFeature
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
/**
* Base fragment extended by [BrowserFragment].
@ -165,6 +167,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
private var fullScreenMediaFeature =
ViewBoundFeatureWrapper<MediaFullscreenOrientationFeature>()
private var fullScreenMediaSessionFeature =
ViewBoundFeatureWrapper<MediaSessionFullscreenFeature>()
private val searchFeature = ViewBoundFeatureWrapper<SearchFeature>()
private var pipFeature: PictureInPictureFeature? = null
@ -392,14 +396,25 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
view = view
)
fullScreenMediaFeature.set(
feature = MediaFullscreenOrientationFeature(
requireActivity(),
context.components.core.store
),
owner = this,
view = view
)
if (newMediaSessionApi) {
fullScreenMediaSessionFeature.set(
feature = MediaSessionFullscreenFeature(
requireActivity(),
context.components.core.store
),
owner = this,
view = view
)
} else {
fullScreenMediaFeature.set(
feature = MediaFullscreenOrientationFeature(
requireActivity(),
context.components.core.store
),
owner = this,
view = view
)
}
val downloadFeature = DownloadsFeature(
context.applicationContext,

@ -39,7 +39,6 @@ import mozilla.components.concept.fetch.Client
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
import mozilla.components.feature.downloads.DownloadMiddleware
import mozilla.components.feature.logins.exceptions.LoginExceptionStorage
import mozilla.components.feature.media.middleware.MediaMiddleware
import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.WebAppShortcutManager
@ -74,7 +73,6 @@ import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.media.MediaService
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry
import org.mozilla.fenix.settings.SupportUtils
@ -82,6 +80,11 @@ import org.mozilla.fenix.settings.advanced.getSelectedLocale
import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.utils.getUndoDelay
import java.util.concurrent.TimeUnit
import mozilla.components.feature.media.MediaSessionFeature
import mozilla.components.feature.media.middleware.MediaMiddleware
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.media.MediaService
import org.mozilla.fenix.media.MediaSessionService
/**
* Component group for all core browser functionality.
@ -179,10 +182,9 @@ class Core(
* The [BrowserStore] holds the global [BrowserState].
*/
val store by lazyMonitored {
BrowserStore(
middleware = listOf(
val middlewareList =
mutableListOf(
RecentlyClosedMiddleware(context, RECENTLY_CLOSED_MAX, engine),
MediaMiddleware(context, MediaService::class.java),
DownloadMiddleware(context, DownloadService::class.java),
ReaderViewMiddleware(),
TelemetryMiddleware(
@ -199,7 +201,14 @@ class Core(
migration = SearchMigration(context)
),
RecordingDevicesMiddleware(context)
) + EngineMiddleware.create(engine, ::findSessionById)
)
if (!newMediaSessionApi) {
middlewareList.add(MediaMiddleware(context, MediaService::class.java))
}
BrowserStore(
middleware = middlewareList + EngineMiddleware.create(engine, ::findSessionById)
)
}
@ -278,6 +287,10 @@ class Core(
context, engine, icons, R.drawable.ic_status_logo,
permissionStorage.permissionsStorage, HomeActivity::class.java
)
if (newMediaSessionApi) {
MediaSessionFeature(context, MediaSessionService::class.java, store).start()
}
}
}

@ -7,24 +7,26 @@ package org.mozilla.fenix.home.intent
import android.content.Intent
import androidx.navigation.NavController
import mozilla.components.feature.media.service.AbstractMediaService
import mozilla.components.feature.media.service.AbstractMediaSessionService
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.ext.components
/**
* When the media notification is clicked we need to switch to the tab where the audio/video is
* playing. This intent has the following informations:
* action - [AbstractMediaService.Companion.ACTION_SWITCH_TAB]
* extra string for the tab id - [AbstractMediaService.Companion.EXTRA_TAB_ID]
* action - [AbstractMediaSessionService.Companion.ACTION_SWITCH_TAB]
* extra string for the tab id - [AbstractMediaSessionService.Companion.EXTRA_TAB_ID]
*/
class OpenSpecificTabIntentProcessor(
private val activity: HomeActivity
) : HomeIntentProcessor {
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
if (intent.action == AbstractMediaService.Companion.ACTION_SWITCH_TAB) {
if (intent.action == getAction()) {
val sessionManager = activity.components.core.sessionManager
val sessionId = intent.extras?.getString(AbstractMediaService.Companion.EXTRA_TAB_ID)
val sessionId = intent.extras?.getString(getTabId())
val session = sessionId?.let { sessionManager.findSessionById(it) }
if (session != null) {
sessionManager.select(session)
@ -36,3 +38,19 @@ class OpenSpecificTabIntentProcessor(
return false
}
}
private fun getAction(): String {
return if (newMediaSessionApi) {
AbstractMediaSessionService.Companion.ACTION_SWITCH_TAB
} else {
AbstractMediaService.Companion.ACTION_SWITCH_TAB
}
}
private fun getTabId(): String {
return if (newMediaSessionApi) {
AbstractMediaSessionService.Companion.EXTRA_TAB_ID
} else {
AbstractMediaService.Companion.EXTRA_TAB_ID
}
}

@ -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.AbstractMediaSessionService
import org.mozilla.fenix.ext.components
/**
* [AbstractMediaSessionService] implementation for injecting [BrowserStore] singleton.
*/
class MediaSessionService : AbstractMediaSessionService() {
override val store: BrowserStore by lazy { components.core.store }
}

@ -13,7 +13,7 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.AppCompatImageButton
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.tab_tray_grid_item.view.*
import mozilla.components.browser.state.state.MediaState
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.browser.tabstray.TabsTrayStyling
@ -21,24 +21,27 @@ import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
import mozilla.components.concept.base.images.ImageLoadRequest
import mozilla.components.concept.base.images.ImageLoader
import mozilla.components.concept.engine.mediasession.MediaSession
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.feature.media.ext.pauseIfPlaying
import mozilla.components.feature.media.ext.playIfPaused
import mozilla.components.support.base.observer.Observable
import org.mozilla.fenix.R
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.getMediaStateForSession
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.removeAndDisable
import org.mozilla.fenix.ext.removeTouchDelegate
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.utils.Do
import kotlin.math.max
import mozilla.components.browser.state.state.MediaState
import mozilla.components.feature.media.ext.pauseIfPlaying
import mozilla.components.feature.media.ext.playIfPaused
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.ext.getMediaStateForSession
import org.mozilla.fenix.utils.Do
/**
* A RecyclerView ViewHolder implementation for "tab" items.
@ -68,6 +71,7 @@ class TabTrayViewHolder(
/**
* Displays the data of the given session and notifies the given observable about events.
*/
@Suppress("ComplexMethod", "LongMethod")
override fun bind(
tab: Tab,
isSelected: Boolean,
@ -94,49 +98,98 @@ class TabTrayViewHolder(
// Media state
playPauseButtonView.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS)
with(playPauseButtonView) {
invalidate()
Do exhaustive when (store.state.getMediaStateForSession(tab.id)) {
MediaState.State.PAUSED -> {
showAndEnable()
contentDescription =
context.getString(R.string.mozac_feature_media_notification_action_play)
setImageDrawable(
AppCompatResources.getDrawable(context, R.drawable.media_state_play)
)
}
MediaState.State.PLAYING -> {
showAndEnable()
contentDescription =
context.getString(R.string.mozac_feature_media_notification_action_pause)
setImageDrawable(
AppCompatResources.getDrawable(context, R.drawable.media_state_pause)
)
if (newMediaSessionApi) {
with(playPauseButtonView) {
invalidate()
val sessionState = store.state.findTabOrCustomTab(tab.id)
when (sessionState?.mediaSessionState?.playbackState) {
MediaSession.PlaybackState.PAUSED -> {
showAndEnable()
contentDescription =
context.getString(R.string.mozac_feature_media_notification_action_play)
setImageDrawable(
AppCompatResources.getDrawable(context, R.drawable.media_state_play)
)
}
MediaSession.PlaybackState.PLAYING -> {
showAndEnable()
contentDescription =
context.getString(R.string.mozac_feature_media_notification_action_pause)
setImageDrawable(
AppCompatResources.getDrawable(context, R.drawable.media_state_pause)
)
}
else -> {
removeTouchDelegate()
removeAndDisable()
}
}
MediaState.State.NONE -> {
removeTouchDelegate()
removeAndDisable()
setOnClickListener {
when (sessionState?.mediaSessionState?.playbackState) {
MediaSession.PlaybackState.PLAYING -> {
metrics.track(Event.TabMediaPause)
sessionState.mediaSessionState?.controller?.pause()
}
MediaSession.PlaybackState.PAUSED -> {
metrics.track(Event.TabMediaPlay)
sessionState.mediaSessionState?.controller?.play()
}
else -> throw AssertionError(
"Play/Pause button clicked without play/pause state."
)
}
}
}
}
} else {
with(playPauseButtonView) {
invalidate()
Do exhaustive when (store.state.getMediaStateForSession(tab.id)) {
MediaState.State.PAUSED -> {
showAndEnable()
contentDescription =
context.getString(R.string.mozac_feature_media_notification_action_play)
setImageDrawable(
AppCompatResources.getDrawable(context, R.drawable.media_state_play)
)
}
playPauseButtonView.setOnClickListener {
Do exhaustive when (store.state.getMediaStateForSession(tab.id)) {
MediaState.State.PLAYING -> {
metrics.track(Event.TabMediaPause)
store.state.media.pauseIfPlaying()
}
MediaState.State.PLAYING -> {
showAndEnable()
contentDescription =
context.getString(R.string.mozac_feature_media_notification_action_pause)
setImageDrawable(
AppCompatResources.getDrawable(context, R.drawable.media_state_pause)
)
}
MediaState.State.PAUSED -> {
metrics.track(Event.TabMediaPlay)
store.state.media.playIfPaused()
MediaState.State.NONE -> {
removeTouchDelegate()
removeAndDisable()
}
}
}
playPauseButtonView.setOnClickListener {
Do exhaustive when (store.state.getMediaStateForSession(tab.id)) {
MediaState.State.PLAYING -> {
metrics.track(Event.TabMediaPause)
store.state.media.pauseIfPlaying()
}
MediaState.State.NONE -> throw AssertionError(
"Play/Pause button clicked without play/pause state."
)
MediaState.State.PAUSED -> {
metrics.track(Event.TabMediaPlay)
store.state.media.playIfPaused()
}
MediaState.State.NONE -> throw AssertionError(
"Play/Pause button clicked without play/pause state."
)
}
}
}

@ -14,12 +14,14 @@ import io.mockk.verifyOrder
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.feature.media.service.AbstractMediaService
import mozilla.components.feature.media.service.AbstractMediaSessionService
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ -78,8 +80,13 @@ class OpenSpecificTabIntentProcessorTest {
@Test
fun `GIVEN an intent with correct action and extra string, WHEN it is processed, THEN session should be selected and openToBrowser should be called`() {
val intent = Intent().apply {
action = AbstractMediaService.Companion.ACTION_SWITCH_TAB
putExtra(AbstractMediaService.Companion.EXTRA_TAB_ID, TEST_SESSION_ID)
if (newMediaSessionApi) {
action = AbstractMediaSessionService.Companion.ACTION_SWITCH_TAB
putExtra(AbstractMediaSessionService.Companion.EXTRA_TAB_ID, TEST_SESSION_ID)
} else {
action = AbstractMediaService.Companion.ACTION_SWITCH_TAB
putExtra(AbstractMediaService.Companion.EXTRA_TAB_ID, TEST_SESSION_ID)
}
}
val sessionManager: SessionManager = mockk(relaxed = true)
val session: Session = mockk(relaxed = true)

@ -15,17 +15,23 @@ import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.MediaSessionState
import mozilla.components.browser.state.state.MediaState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.base.images.ImageLoadRequest
import mozilla.components.concept.base.images.ImageLoader
import mozilla.components.concept.engine.mediasession.MediaSession
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.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ -36,6 +42,8 @@ class TabTrayViewHolderTest {
private lateinit var view: View
@MockK private lateinit var imageLoader: ImageLoader
@MockK private lateinit var store: BrowserStore
@MockK private lateinit var sessionState: SessionState
@MockK private lateinit var mediaSessionState: MediaSessionState
@MockK private lateinit var metrics: MetricController
private var state = BrowserState()
@ -74,14 +82,33 @@ class TabTrayViewHolderTest {
id = "123",
url = "https://example.com"
)
state = state.copy(
media = MediaState(
aggregate = MediaState.Aggregate(
activeTabId = "123",
state = MediaState.State.PAUSED
if (newMediaSessionApi) {
state = state.copy(
tabs = listOf(
TabSessionState(
id = "123",
content = ContentState(
url = "https://example.com",
searchTerms = "search terms"
),
mediaSessionState = mediaSessionState
)
)
)
)
every { mediaSessionState.playbackState } answers { MediaSession.PlaybackState.PAUSED }
} else {
state = state.copy(
media = MediaState(
aggregate = MediaState.Aggregate(
activeTabId = "123",
state = MediaState.State.PAUSED
)
)
)
}
tabViewHolder.bind(tab, false, mockk(), mockk())
assertEquals("Play", playPauseButtonView.contentDescription)
@ -96,14 +123,33 @@ class TabTrayViewHolderTest {
id = "123",
url = "https://example.com"
)
state = state.copy(
media = MediaState(
aggregate = MediaState.Aggregate(
activeTabId = "123",
state = MediaState.State.PLAYING
if (newMediaSessionApi) {
state = state.copy(
tabs = listOf(
TabSessionState(
id = "123",
content = ContentState(
url = "https://example.com",
searchTerms = "search terms"
),
mediaSessionState = mediaSessionState
)
)
)
)
every { mediaSessionState.playbackState } answers { MediaSession.PlaybackState.PLAYING }
} else {
state = state.copy(
media = MediaState(
aggregate = MediaState.Aggregate(
activeTabId = "123",
state = MediaState.State.PLAYING
)
)
)
}
tabViewHolder.bind(tab, false, mockk(), mockk())
assertEquals("Pause", playPauseButtonView.contentDescription)

Loading…
Cancel
Save