Commit a06bd664 authored by Tiger Oakes's avatar Tiger Oakes
Browse files

Move PIP to BrowserStore

parent 23598e41
......@@ -250,10 +250,15 @@ sealed class ContentAction : BrowserAction() {
data class ConsumeSearchRequestAction(val sessionId: String) : ContentAction()
/**
* Updates the [fullScreenEnabled] with the given [sessionId].
* Updates [fullScreenEnabled] with the given [sessionId].
*/
data class FullScreenChangedAction(val sessionId: String, val fullScreenEnabled: Boolean) : ContentAction()
/**
* Updates [pipEnabled] with the given [sessionId].
*/
data class PictureInPictureChangedAction(val sessionId: String, val pipEnabled: Boolean) : ContentAction()
/**
* Updates the [layoutInDisplayCutoutMode] with the given [sessionId].
*
......
......@@ -104,6 +104,9 @@ internal object ContentStateReducer {
is ContentAction.FullScreenChangedAction -> updateContentState(state, action.sessionId) {
it.copy(fullScreen = action.fullScreenEnabled)
}
is ContentAction.PictureInPictureChangedAction -> updateContentState(state, action.sessionId) {
it.copy(pictureInPictureEnabled = action.pipEnabled)
}
is ContentAction.ViewportFitChangedAction -> updateContentState(state, action.sessionId) {
it.copy(layoutInDisplayCutoutMode = action.layoutInDisplayCutoutMode)
}
......
......@@ -41,6 +41,7 @@ import mozilla.components.concept.engine.window.WindowRequest
* @property canGoForward whether or not there's an history item to navigate forward to.
* @property webAppManifest the Web App Manifest for the currently visited page (or null).
* @property firstContentfulPaint whether or not the first contentful paint has happened.
* @property pictureInPictureEnabled True if the session is being displayed in PIP mode.
*/
data class ContentState(
val url: String,
......@@ -64,5 +65,6 @@ data class ContentState(
val canGoForward: Boolean = false,
val webAppManifest: WebAppManifest? = null,
val firstContentfulPaint: Boolean = false,
val history: HistoryState = HistoryState()
val history: HistoryState = HistoryState(),
val pictureInPictureEnabled: Boolean = false
)
......@@ -8,32 +8,33 @@ import android.app.Activity
import android.app.PictureInPictureParams
import android.content.pm.PackageManager
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import androidx.annotation.RequiresApi
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.runWithSessionIdOrSelected
import kotlinx.coroutines.Job
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.base.crash.CrashReporting
import mozilla.components.support.base.log.logger.Logger
/**
* A simple implementation of Picture-in-picture mode if on a supported platform.
*
* @param sessionManager Session Manager for observing the selected session's fullscreen mode changes.
* @param store Browser Store for observing the selected session's fullscreen mode changes.
* @param activity the activity with the EngineView for calling PIP mode when required; the AndroidX Fragment
* doesn't support this.
* @param crashReporting Instance of `CrashReporting` to record unexpected caught exceptions
* @param customTabSessionId ID of custom tab session.
* @param pipChanged a change listener that allows the calling app to perform changes based on PIP mode.
* @param tabId ID of tab or custom tab session.
*/
class PictureInPictureFeature(
private val sessionManager: SessionManager,
private val store: BrowserStore,
private val activity: Activity,
private val crashReporting: CrashReporting? = null,
private val customTabSessionId: String? = null,
private val pipChanged: ((Boolean) -> Unit?)? = null
private val tabId: String? = null
) {
internal val logger = Logger("PictureInPictureFeature")
private val hasSystemFeature = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
private val hasSystemFeature = SDK_INT >= Build.VERSION_CODES.N &&
activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
fun onHomePressed(): Boolean {
......@@ -41,10 +42,8 @@ class PictureInPictureFeature(
return false
}
val fullScreenMode =
sessionManager.runWithSessionIdOrSelected(customTabSessionId) { session ->
session.fullScreenMode
}
val session = store.state.findTabOrCustomTabOrSelectedTab(tabId)
val fullScreenMode = session?.content?.fullScreen == true
return fullScreenMode && try {
enterPipModeCompat()
} catch (e: IllegalStateException) {
......@@ -57,12 +56,13 @@ class PictureInPictureFeature(
}
}
/**
* Enter Picture-in-Picture mode.
*/
fun enterPipModeCompat() = when {
!hasSystemFeature -> false
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ->
enterPipModeForO()
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ->
enterPipModeForN()
SDK_INT >= Build.VERSION_CODES.O -> enterPipModeForO()
SDK_INT >= Build.VERSION_CODES.N -> enterPipModeForN()
else -> false
}
......@@ -70,12 +70,19 @@ class PictureInPictureFeature(
private fun enterPipModeForO() =
activity.enterPictureInPictureMode(PictureInPictureParams.Builder().build())
@Suppress("DEPRECATION")
@Suppress("Deprecation")
@RequiresApi(Build.VERSION_CODES.N)
private fun enterPipModeForN() = run {
activity.enterPictureInPictureMode()
true
}
fun onPictureInPictureModeChanged(enabled: Boolean) = pipChanged?.invoke(enabled)
/**
* Should be called when then system informs you of changes to and from picture-in-picture mode.
* @param enabled True if the activity is in picture-in-picture mode.
*/
fun onPictureInPictureModeChanged(enabled: Boolean): Job {
val sessionId = tabId ?: store.state.selectedTabId.orEmpty()
return store.dispatch(ContentAction.PictureInPictureChangedAction(sessionId, enabled))
}
}
......@@ -8,8 +8,11 @@ import android.app.Activity
import android.content.pm.PackageManager
import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.base.crash.CrashReporting
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
......@@ -25,19 +28,13 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.verification.VerificationMode
import org.robolectric.annotation.Config
import java.lang.IllegalStateException
private interface PipChangedCallback : (Boolean) -> Unit
@RunWith(AndroidJUnit4::class)
class PictureInPictureFeatureTest {
private val sessionManager: SessionManager = mock()
private val selectedSession: Session = mock()
private val crashReporting: CrashReporting = mock()
private val activity: Activity = Mockito.mock(Activity::class.java, Mockito.RETURNS_DEEP_STUBS)
......@@ -50,14 +47,15 @@ class PictureInPictureFeatureTest {
@Test
@Config(sdk = [Build.VERSION_CODES.M])
fun `on home pressed without system feature on android m and lower`() {
val store = mock<BrowserStore>()
whenever(activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE))
.thenReturn(false)
val pictureInPictureFeature =
spy(PictureInPictureFeature(sessionManager, activity, crashReporting))
spy(PictureInPictureFeature(store, activity, crashReporting))
assertFalse(pictureInPictureFeature.onHomePressed())
verifyZeroInteractions(sessionManager)
verifyZeroInteractions(store)
verifyZeroInteractions(activity.packageManager)
verify(pictureInPictureFeature, never()).enterPipModeCompat()
}
......@@ -65,78 +63,88 @@ class PictureInPictureFeatureTest {
@Test
@Config(sdk = [Build.VERSION_CODES.N])
fun `on home pressed without system feature on android n and above`() {
val store = mock<BrowserStore>()
whenever(activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE))
.thenReturn(false)
val pictureInPictureFeature =
spy(PictureInPictureFeature(sessionManager, activity, crashReporting))
spy(PictureInPictureFeature(store, activity, crashReporting))
assertFalse(pictureInPictureFeature.onHomePressed())
verifyZeroInteractions(sessionManager)
verifyZeroInteractions(store)
verify(activity.packageManager).hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
verify(pictureInPictureFeature, never()).enterPipModeCompat()
}
@Test
fun `on home pressed without a selected session`() {
val store = BrowserStore()
val pictureInPictureFeature =
spy(PictureInPictureFeature(sessionManager, activity, crashReporting))
spy(PictureInPictureFeature(store, activity, crashReporting))
assertFalse(pictureInPictureFeature.onHomePressed())
verify(sessionManager).selectedSession
verify(activity.packageManager).hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
verify(pictureInPictureFeature, never()).enterPipModeCompat()
}
@Test
fun `on home pressed with a selected session without a fullscreen mode`() {
val selectedSession = createTab("https://mozilla.org").copyWithFullScreen(false)
val store = BrowserStore(
BrowserState(
tabs = listOf(selectedSession),
selectedTabId = selectedSession.id
)
)
val pictureInPictureFeature =
spy(PictureInPictureFeature(sessionManager, activity, crashReporting))
whenever(selectedSession.fullScreenMode).thenReturn(false)
whenever(sessionManager.selectedSession).thenReturn(selectedSession)
spy(PictureInPictureFeature(store, activity, crashReporting))
assertFalse(selectedSession.content.fullScreen)
assertFalse(pictureInPictureFeature.onHomePressed())
verify(sessionManager).selectedSession
verify(selectedSession).fullScreenMode
verify(pictureInPictureFeature, never()).enterPipModeCompat()
}
@Test
fun `on home pressed with a selected session in fullscreen and without pip mode`() {
val selectedSession = createTab("https://mozilla.org").copyWithFullScreen(true)
val store = BrowserStore(
BrowserState(
tabs = listOf(selectedSession),
selectedTabId = selectedSession.id
)
)
val pictureInPictureFeature =
spy(PictureInPictureFeature(sessionManager, activity, crashReporting))
spy(PictureInPictureFeature(store, activity, crashReporting))
whenever(selectedSession.fullScreenMode).thenReturn(true)
whenever(sessionManager.selectedSession).thenReturn(selectedSession)
doReturn(false).`when`(pictureInPictureFeature).enterPipModeCompat()
assertFalse(pictureInPictureFeature.onHomePressed())
verify(sessionManager).selectedSession
verify(selectedSession).fullScreenMode
verify(pictureInPictureFeature).enterPipModeCompat()
}
@Test
fun `on home pressed with a selected session in fullscreen and with pip mode`() {
val selectedSession = createTab("https://mozilla.org").copyWithFullScreen(true)
val store = BrowserStore(
BrowserState(
tabs = listOf(selectedSession),
selectedTabId = selectedSession.id
)
)
val pictureInPictureFeature =
spy(PictureInPictureFeature(sessionManager, activity, crashReporting))
spy(PictureInPictureFeature(store, activity, crashReporting))
whenever(selectedSession.fullScreenMode).thenReturn(true)
whenever(sessionManager.selectedSession).thenReturn(selectedSession)
doReturn(true).`when`(pictureInPictureFeature).enterPipModeCompat()
assertTrue(pictureInPictureFeature.onHomePressed())
verify(sessionManager).selectedSession
verify(selectedSession).fullScreenMode
verify(pictureInPictureFeature).enterPipModeCompat()
}
@Test
@Config(sdk = [Build.VERSION_CODES.M])
fun `enter pip mode compat on android m and below`() {
val pictureInPictureFeature =
PictureInPictureFeature(sessionManager, activity, crashReporting)
val store = mock<BrowserStore>()
val pictureInPictureFeature = PictureInPictureFeature(store, activity, crashReporting)
assertFalse(pictureInPictureFeature.enterPipModeCompat())
}
......@@ -148,7 +156,7 @@ class PictureInPictureFeatureTest {
.thenReturn(false)
val pictureInPictureFeature =
PictureInPictureFeature(sessionManager, activity, crashReporting)
PictureInPictureFeature(mock(), activity, crashReporting)
assertFalse(pictureInPictureFeature.enterPipModeCompat())
verify(activity, never()).enterPictureInPictureMode(any())
......@@ -158,14 +166,20 @@ class PictureInPictureFeatureTest {
@Test
@Config(sdk = [Build.VERSION_CODES.O])
fun `enter pip mode compat with system feature on android o but entering throws exception`() {
val selectedSession = createTab("https://mozilla.org").copyWithFullScreen(true)
val store = BrowserStore(
BrowserState(
tabs = listOf(selectedSession),
selectedTabId = selectedSession.id
)
)
whenever(activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE))
.thenReturn(true)
whenever(activity.enterPictureInPictureMode(any())).thenThrow(IllegalStateException())
whenever(selectedSession.fullScreenMode).thenReturn(true)
whenever(sessionManager.selectedSession).thenReturn(selectedSession)
val pictureInPictureFeature =
PictureInPictureFeature(sessionManager, activity, crashReporting)
PictureInPictureFeature(store, activity, crashReporting)
assertFalse(pictureInPictureFeature.onHomePressed())
verify(crashReporting).submitCaughtException(any<IllegalStateException>())
}
......@@ -174,7 +188,7 @@ class PictureInPictureFeatureTest {
@Config(sdk = [Build.VERSION_CODES.O])
fun `enter pip mode compat on android o and above`() {
val pictureInPictureFeature =
PictureInPictureFeature(sessionManager, activity, crashReporting)
PictureInPictureFeature(mock(), activity, crashReporting)
whenever(activity.enterPictureInPictureMode(any())).thenReturn(true)
......@@ -186,39 +200,47 @@ class PictureInPictureFeatureTest {
@Config(sdk = [Build.VERSION_CODES.N])
fun `enter pip mode compat on android n and above`() {
val pictureInPictureFeature =
PictureInPictureFeature(sessionManager, activity, crashReporting)
PictureInPictureFeature(mock(), activity, crashReporting)
assertTrue(pictureInPictureFeature.enterPipModeCompat())
verifyDeprecatedPictureInPictureMode(activity)
}
@Test
fun `on pip mode changed`() {
val pipChangedCallback: PipChangedCallback = mock()
fun `on pip mode changed`() = runBlocking {
val selectedSession = createTab("https://mozilla.org").copyWithFullScreen(true)
val store = BrowserStore(
BrowserState(
tabs = listOf(selectedSession),
selectedTabId = selectedSession.id
)
)
val pipFeature = PictureInPictureFeature(
sessionManager,
store,
activity,
crashReporting,
null,
pipChangedCallback
tabId = null
)
pipFeature.onPictureInPictureModeChanged(true)
verify(pipChangedCallback).invoke(true)
pipFeature.onPictureInPictureModeChanged(true).join()
assertTrue(store.state.tabs[0].content.pictureInPictureEnabled)
pipFeature.onPictureInPictureModeChanged(false)
verify(pipChangedCallback).invoke(false)
pipFeature.onPictureInPictureModeChanged(false).join()
assertFalse(store.state.tabs[0].content.pictureInPictureEnabled)
verifyNoMoreInteractions(sessionManager)
verify(activity, never()).enterPictureInPictureMode(any())
verifyDeprecatedPictureInPictureMode(activity, never())
}
}
@Suppress("DEPRECATION")
private fun verifyDeprecatedPictureInPictureMode(
activity: Activity,
mode: VerificationMode = times(1)
) {
verify(activity, mode).enterPictureInPictureMode()
@Suppress("Deprecation")
private fun verifyDeprecatedPictureInPictureMode(
activity: Activity,
mode: VerificationMode = times(1)
) {
verify(activity, mode).enterPictureInPictureMode()
}
@Suppress("Unchecked_Cast")
private fun <T : SessionState> T.copyWithFullScreen(fullScreen: Boolean): T =
createCopy(content = content.copy(fullScreen = fullScreen)) as T
}
......@@ -49,6 +49,13 @@ permalink: /changelog/
* Added `Uri.sameOriginAs` to check if two Uris have the same origin (same host, same port).
* Added `Uri.isInScope` to check if a Uri is within one of the given scopes.
* **browser-state**
* Added `ContentState.pictureInPictureEnabled` to track if Picture in Picture mode is in use.
* **feature-session**
* ⚠️ **This is a breaking change**: `PictureInPictureFeature` now takes `BrowserStore` instead of `SessionManager`.
* ⚠️ **This is a breaking change**: The `pipChanged` callback has been removed. You should now observe `ContentState.pictureInPictureEnabled` instead.
# 44.0.0
* [Commits](https://github.com/mozilla-mobile/android-components/compare/v43.0.0...v44.0.0)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment