diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvent.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvent.kt index 1af13e148e5..499e483861f 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvent.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvent.kt @@ -14,4 +14,5 @@ sealed interface 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 } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt index 2dc16d7f261..909c1c76690 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt @@ -55,6 +55,10 @@ class PictureInPicturePresenter( pipController?.exitPip() } } + is PictureInPictureEvent.SetPipOrientation -> { + Timber.tag(loggerTag.value).d("onOrientationChange: ${event.orientation}") + pipView?.setPipOrientation(event.orientation) + } } } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipView.kt index 90f74a37fd1..aa1ab79dd07 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipView.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipView.kt @@ -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() diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt index 2537eb739ce..8c3cde22472 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt @@ -130,7 +130,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 = { diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt index 26df7c160cb..ec3f6b8f00d 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt @@ -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 @@ -48,6 +48,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 @@ -85,6 +86,8 @@ class ElementCallActivity : private var eventSink: ((CallScreenEvent) -> Unit)? = null + private var pendingPipOrientationChange: Int? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -291,11 +294,20 @@ class ElementCallActivity : } } + override fun setPipOrientation(orientation: Int?) { + pendingPipOrientationChange = orientation + if (isInPictureInPictureMode) { + setPictureInPictureParams(getPictureInPictureParams()) + } + } + @RequiresApi(Build.VERSION_CODES.O) private fun getPictureInPictureParams(): PictureInPictureParams { + // Portrait for calls seems more appropriate as a fallback value + val orientation = pendingPipOrientationChange ?: Configuration.ORIENTATION_PORTRAIT + pendingPipOrientationChange = null 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) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt index febd9191498..976c784abff 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt @@ -251,7 +251,7 @@ class WebViewAudioManager( hasRegisteredCallbacks = true } - } + }, ) Timber.d("Setting androidNativeBridge javascript interface in webview") webView.addJavascriptInterface(webViewAudioDeviceSelectedCallback, "androidNativeBridge") diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt index 12f8dfd00f6..5000956faea 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt @@ -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,13 @@ 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 suspend fun canEnterPip(): Boolean { return suspendCoroutine { continuation -> webView.evaluateJavascript("controls.canEnterPip()") { result -> @@ -29,10 +38,30 @@ class WebViewPipController( } override fun enterPip() { + Timber.d("Adding callback in controls.onPipMediaOrientationUpdate") + webView.evaluateJavascript("controls.onPipMediaOrientationUpdate = (orientation) => { androidPipBridge.setPipOrientation(orientation); };", null) + webView.evaluateJavascript("controls.enablePip()", null) } override fun exitPip() { 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) + } } diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipView.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipView.kt index 55e94312bfc..adfcde2c388 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipView.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipView.kt @@ -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) } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/PictureInPictureParamsExt.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/PictureInPictureParamsExt.kt new file mode 100644 index 00000000000..e5ac0c32649 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/PictureInPictureParamsExt.kt @@ -0,0 +1,26 @@ +/* + * 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.util.Rational + +/** + * Set the aspect ratio of the Picture-in-Picture mode based on the current orientation of the other user's device. + */ +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) +}