From 2ff7ba75c768e0ae6f605e01fc3f20f4e45e3126 Mon Sep 17 00:00:00 2001 From: Roger Yang Date: Tue, 8 Dec 2020 11:04:56 -0500 Subject: [PATCH] Closes #16896: Integrate new MediaSession API to nightly or debug builds (#16909) --- app/src/main/AndroidManifest.xml | 3 + .../java/org/mozilla/fenix/FeatureFlags.kt | 5 + .../fenix/browser/BaseBrowserFragment.kt | 33 +++-- .../java/org/mozilla/fenix/components/Core.kt | 25 +++- .../intent/OpenSpecificTabIntentProcessor.kt | 26 +++- .../fenix/media/MediaSessionService.kt | 16 +++ .../fenix/tabtray/TabTrayViewHolder.kt | 131 ++++++++++++------ .../OpenSpecificTabIntentProcessorTest.kt | 11 +- .../fenix/tabtray/TabTrayViewHolderTest.kt | 70 ++++++++-- 9 files changed, 248 insertions(+), 72 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/media/MediaSessionService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6c4d1e1cf..efb94c3c0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -232,6 +232,9 @@ + + () private var fullScreenMediaFeature = ViewBoundFeatureWrapper() + private var fullScreenMediaSessionFeature = + ViewBoundFeatureWrapper() private val searchFeature = ViewBoundFeatureWrapper() 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, diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index 405bf5c8d..1161ae7fa 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -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() + } } } diff --git a/app/src/main/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessor.kt b/app/src/main/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessor.kt index 618274272..4e91e5ef4 100644 --- a/app/src/main/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessor.kt @@ -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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/media/MediaSessionService.kt b/app/src/main/java/org/mozilla/fenix/media/MediaSessionService.kt new file mode 100644 index 000000000..0c08393e1 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/media/MediaSessionService.kt @@ -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 } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt index 49bb453e8..1b811b21a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt @@ -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) - ) + + 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.PLAYING -> { - showAndEnable() - contentDescription = - context.getString(R.string.mozac_feature_media_notification_action_pause) - setImageDrawable( - AppCompatResources.getDrawable(context, R.drawable.media_state_pause) - ) - } + setOnClickListener { + when (sessionState?.mediaSessionState?.playbackState) { + MediaSession.PlaybackState.PLAYING -> { + metrics.track(Event.TabMediaPause) + sessionState.mediaSessionState?.controller?.pause() + } - MediaState.State.NONE -> { - removeTouchDelegate() - removeAndDisable() + 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.NONE -> { + removeTouchDelegate() + removeAndDisable() + } } + } - MediaState.State.PAUSED -> { - metrics.track(Event.TabMediaPlay) - store.state.media.playIfPaused() + playPauseButtonView.setOnClickListener { + Do exhaustive when (store.state.getMediaStateForSession(tab.id)) { + MediaState.State.PLAYING -> { + metrics.track(Event.TabMediaPause) + store.state.media.pauseIfPlaying() + } + + MediaState.State.PAUSED -> { + metrics.track(Event.TabMediaPlay) + store.state.media.playIfPaused() + } + + MediaState.State.NONE -> throw AssertionError( + "Play/Pause button clicked without play/pause state." + ) } - - MediaState.State.NONE -> throw AssertionError( - "Play/Pause button clicked without play/pause state." - ) } } diff --git a/app/src/test/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessorTest.kt b/app/src/test/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessorTest.kt index 977e737f6..7dc14cc99 100644 --- a/app/src/test/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessorTest.kt @@ -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) diff --git a/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayViewHolderTest.kt index f1672e82e..f55d4d6cc 100644 --- a/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayViewHolderTest.kt @@ -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)