mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 08:15:02 +00:00
Make PIP react to camera orientation changes in the other participant's device (#6951)
* Make PIP react to camera orientation changes in the other participant's device Adds a `controls.onPipMediaOrientationUpdate` callback for the WebView that will trigger a new `pipParamsBuilder.setAspectRatioFromOrientation(orientation)` method * Fix warning about `PictureInPictureParams.Builder.setAspectRatio` only being available on API 26+
This commit is contained in:
committed by
GitHub
parent
2b3aeee1c6
commit
614619e306
+2
@@ -11,7 +11,9 @@ package io.element.android.features.call.impl.pip
|
||||
import io.element.android.features.call.impl.utils.PipController
|
||||
|
||||
sealed interface PictureInPictureEvent {
|
||||
data object OnCallStarted : PictureInPictureEvent
|
||||
data class SetPipController(val pipController: PipController) : PictureInPictureEvent
|
||||
data object EnterPictureInPicture : PictureInPictureEvent
|
||||
data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvent
|
||||
data class SetPipOrientation(val orientation: Int?) : PictureInPictureEvent
|
||||
}
|
||||
|
||||
+7
@@ -38,6 +38,9 @@ class PictureInPicturePresenter(
|
||||
|
||||
fun handleEvent(event: PictureInPictureEvent) {
|
||||
when (event) {
|
||||
is PictureInPictureEvent.OnCallStarted -> {
|
||||
pipController?.setupPipCallbacks()
|
||||
}
|
||||
is PictureInPictureEvent.SetPipController -> {
|
||||
pipController = event.pipController
|
||||
}
|
||||
@@ -55,6 +58,10 @@ class PictureInPicturePresenter(
|
||||
pipController?.exitPip()
|
||||
}
|
||||
}
|
||||
is PictureInPictureEvent.SetPipOrientation -> {
|
||||
Timber.tag(loggerTag.value).d("onOrientationChange: ${event.orientation}")
|
||||
pipView?.setPipOrientation(event.orientation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -9,6 +9,7 @@
|
||||
package io.element.android.features.call.impl.pip
|
||||
|
||||
interface PipView {
|
||||
fun setPipOrientation(orientation: Int?)
|
||||
fun setPipParams()
|
||||
fun enterPipMode(): Boolean
|
||||
fun hangUp()
|
||||
|
||||
+7
-1
@@ -118,6 +118,7 @@ internal fun CallScreenView(
|
||||
if (webViewAudioManager?.isInCallMode?.get() == false) {
|
||||
Timber.d("URL $url is loaded, starting in-call audio mode")
|
||||
webViewAudioManager?.onCallStarted()
|
||||
pipState.eventSink(PictureInPictureEvent.OnCallStarted)
|
||||
} else {
|
||||
Timber.d("Can't start in-call audio mode since the app is already in it.")
|
||||
}
|
||||
@@ -130,7 +131,12 @@ internal fun CallScreenView(
|
||||
onInvalidAudioDeviceAdded = { invalidAudioDeviceReason = it },
|
||||
)
|
||||
state.eventSink(CallScreenEvent.SetupMessageChannels(interceptor))
|
||||
val pipController = WebViewPipController(webView)
|
||||
val pipController = WebViewPipController(
|
||||
webView = webView,
|
||||
updatePipOrientation = { orientation ->
|
||||
pipState.eventSink(PictureInPictureEvent.SetPipOrientation(orientation))
|
||||
}
|
||||
)
|
||||
pipState.eventSink(PictureInPictureEvent.SetPipController(pipController))
|
||||
},
|
||||
onDestroyWebView = {
|
||||
|
||||
+13
-3
@@ -11,9 +11,9 @@ package io.element.android.features.call.impl.ui
|
||||
import android.Manifest
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Rational
|
||||
import android.view.WindowManager
|
||||
import android.webkit.PermissionRequest
|
||||
import androidx.activity.compose.setContent
|
||||
@@ -49,6 +49,7 @@ import io.element.android.features.call.impl.pip.PipView
|
||||
import io.element.android.features.call.impl.services.CallForegroundService
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.libraries.androidutils.browser.ConsoleMessageLogger
|
||||
import io.element.android.libraries.androidutils.media.setAspectRatioFromOrientation
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.audio.api.AudioFocus
|
||||
@@ -86,6 +87,8 @@ class ElementCallActivity :
|
||||
|
||||
private var eventSink: ((CallScreenEvent) -> Unit)? = null
|
||||
|
||||
private var currentPipOrientation: Int? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -199,6 +202,7 @@ class ElementCallActivity :
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
val onPictureInPictureModeChangedListener = Consumer { _: PictureInPictureModeChangedInfo ->
|
||||
setPipParams()
|
||||
pipEventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(isInPictureInPictureMode))
|
||||
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||
Timber.tag(loggerTag.value).d("Exiting PiP mode: Hangup the call")
|
||||
@@ -296,11 +300,17 @@ class ElementCallActivity :
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPipOrientation(orientation: Int?) {
|
||||
currentPipOrientation = orientation
|
||||
setPictureInPictureParams(getPictureInPictureParams())
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun getPictureInPictureParams(): PictureInPictureParams {
|
||||
// Portrait for calls seems more appropriate as a fallback value
|
||||
val orientation = currentPipOrientation ?: Configuration.ORIENTATION_PORTRAIT
|
||||
return PictureInPictureParams.Builder()
|
||||
// Portrait for calls seems more appropriate
|
||||
.setAspectRatio(Rational(3, 5))
|
||||
.setAspectRatioFromOrientation(orientation)
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
setAutoEnterEnabled(true)
|
||||
|
||||
+1
@@ -9,6 +9,7 @@
|
||||
package io.element.android.features.call.impl.utils
|
||||
|
||||
interface PipController {
|
||||
fun setupPipCallbacks()
|
||||
suspend fun canEnterPip(): Boolean
|
||||
fun enterPip()
|
||||
fun exitPip()
|
||||
|
||||
+1
-1
@@ -250,7 +250,7 @@ class WebViewAudioManager(
|
||||
|
||||
hasRegisteredCallbacks = true
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
Timber.d("Setting androidNativeBridge javascript interface in webview")
|
||||
webView.addJavascriptInterface(webViewAudioDeviceSelectedCallback, "androidNativeBridge")
|
||||
|
||||
+30
@@ -8,7 +8,10 @@
|
||||
|
||||
package io.element.android.features.call.impl.utils
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@@ -18,7 +21,18 @@ import kotlin.coroutines.suspendCoroutine
|
||||
*/
|
||||
class WebViewPipController(
|
||||
private val webView: WebView,
|
||||
updatePipOrientation: (orientation: Int?) -> Unit,
|
||||
) : PipController {
|
||||
init {
|
||||
// Register the JavaScript interface ahead of time to ensure it's available when the WebView content is loaded and tries to call it
|
||||
webView.addJavascriptInterface(PipBridge(updatePipOrientation), "androidPipBridge")
|
||||
}
|
||||
|
||||
override fun setupPipCallbacks() {
|
||||
Timber.d("Adding callback in controls.onPipMediaOrientationUpdate")
|
||||
webView.evaluateJavascript("controls.onPipMediaOrientationUpdate = (orientation) => { androidPipBridge.setPipOrientation(orientation); };", null)
|
||||
}
|
||||
|
||||
override suspend fun canEnterPip(): Boolean {
|
||||
return suspendCoroutine { continuation ->
|
||||
webView.evaluateJavascript("controls.canEnterPip()") { result ->
|
||||
@@ -36,3 +50,19 @@ class WebViewPipController(
|
||||
webView.evaluateJavascript("controls.disablePip()", null)
|
||||
}
|
||||
}
|
||||
|
||||
private class PipBridge(
|
||||
private val onOrientationChange: (orientation: Int?) -> Unit,
|
||||
) {
|
||||
@JavascriptInterface
|
||||
fun setPipOrientation(orientation: String?) {
|
||||
Timber.d("Picture-in-picture orientation changed in webview, orientation: $orientation")
|
||||
// This callback is used to update the picture-in-picture orientation in the WebView when it changes
|
||||
val orientation = when (orientation) {
|
||||
"portrait" -> Configuration.ORIENTATION_PORTRAIT
|
||||
"landscape" -> Configuration.ORIENTATION_LANDSCAPE
|
||||
else -> return
|
||||
}
|
||||
onOrientationChange(orientation)
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -12,10 +12,13 @@ import io.element.android.features.call.impl.utils.PipController
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakePipController(
|
||||
private val setupPipCallbacksResult: () -> Unit = { lambdaError() },
|
||||
private val canEnterPipResult: () -> Boolean = { lambdaError() },
|
||||
private val enterPipResult: () -> Unit = { lambdaError() },
|
||||
private val exitPipResult: () -> Unit = { lambdaError() },
|
||||
) : PipController {
|
||||
override fun setupPipCallbacks() = setupPipCallbacksResult()
|
||||
|
||||
override suspend fun canEnterPip(): Boolean = canEnterPipResult()
|
||||
|
||||
override fun enterPip() = enterPipResult()
|
||||
|
||||
+3
-1
@@ -13,9 +13,11 @@ import io.element.android.tests.testutils.lambda.lambdaError
|
||||
class FakePipView(
|
||||
private val setPipParamsResult: () -> Unit = { lambdaError() },
|
||||
private val enterPipModeResult: () -> Boolean = { lambdaError() },
|
||||
private val handUpResult: () -> Unit = { lambdaError() }
|
||||
private val handUpResult: () -> Unit = { lambdaError() },
|
||||
private val onOrientationChangeResult: (orientation: Int?) -> Unit = { lambdaError() },
|
||||
) : PipView {
|
||||
override fun setPipParams() = setPipParamsResult()
|
||||
override fun enterPipMode(): Boolean = enterPipModeResult()
|
||||
override fun hangUp() = handUpResult()
|
||||
override fun setPipOrientation(orientation: Int?) = onOrientationChangeResult(orientation)
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.media
|
||||
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.util.Rational
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
/**
|
||||
* Set the aspect ratio of the Picture-in-Picture mode based on the current orientation of the other user's device.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun PictureInPictureParams.Builder.setAspectRatioFromOrientation(orientation: Int): PictureInPictureParams.Builder {
|
||||
val aspectRatio = if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
// If landscape orientation, invert the aspect ratio
|
||||
Rational(5, 3)
|
||||
} else {
|
||||
// In any other case, assume portrait orientation
|
||||
Rational(3, 5)
|
||||
}
|
||||
return setAspectRatio(aspectRatio)
|
||||
}
|
||||
Reference in New Issue
Block a user