Element Call: remove top app bar and add it inside the webview instead (#4927)

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa 2025-06-27 08:41:10 +02:00 committed by GitHub
parent c13865b275
commit 3c9b2144cd
16 changed files with 33 additions and 153 deletions

View file

@ -7,16 +7,6 @@
package io.element.android.features.call.impl.pip package io.element.android.features.call.impl.pip
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class PictureInPictureStateProvider : PreviewParameterProvider<PictureInPictureState> {
override val values: Sequence<PictureInPictureState>
get() = sequenceOf(
aPictureInPictureState(supportPip = true),
aPictureInPictureState(supportPip = true, isInPictureInPicture = true),
)
}
fun aPictureInPictureState( fun aPictureInPictureState(
supportPip: Boolean = false, supportPip: Boolean = false,
isInPictureInPicture: Boolean = false, isInPictureInPicture: Boolean = false,

View file

@ -11,6 +11,7 @@ import android.annotation.SuppressLint
import android.util.Log import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.ConsoleMessage import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
import android.webkit.PermissionRequest import android.webkit.PermissionRequest
import android.webkit.WebChromeClient import android.webkit.WebChromeClient
import android.webkit.WebView import android.webkit.WebView
@ -19,7 +20,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -32,11 +32,9 @@ import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.call.impl.R import io.element.android.features.call.impl.R
import io.element.android.features.call.impl.pip.PictureInPictureEvents import io.element.android.features.call.impl.pip.PictureInPictureEvents
import io.element.android.features.call.impl.pip.PictureInPictureState import io.element.android.features.call.impl.pip.PictureInPictureState
import io.element.android.features.call.impl.pip.PictureInPictureStateProvider
import io.element.android.features.call.impl.pip.aPictureInPictureState import io.element.android.features.call.impl.pip.aPictureInPictureState
import io.element.android.features.call.impl.utils.InvalidAudioDeviceReason import io.element.android.features.call.impl.utils.InvalidAudioDeviceReason
import io.element.android.features.call.impl.utils.WebViewAudioManager import io.element.android.features.call.impl.utils.WebViewAudioManager
@ -44,13 +42,11 @@ import io.element.android.features.call.impl.utils.WebViewPipController
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import timber.log.Timber import timber.log.Timber
@ -60,7 +56,6 @@ interface CallScreenNavigator {
fun close() fun close()
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
internal fun CallScreenView( internal fun CallScreenView(
state: CallScreenState, state: CallScreenState,
@ -78,19 +73,6 @@ internal fun CallScreenView(
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
topBar = {
if (!pipState.isInPictureInPicture) {
TopAppBar(
title = { Text(stringResource(R.string.element_call)) },
navigationIcon = {
BackButton(
imageVector = if (pipState.supportPip) CompoundIcons.ArrowLeft() else CompoundIcons.Close(),
onClick = ::handleBack,
)
}
)
}
}
) { padding -> ) { padding ->
BackHandler { BackHandler {
handleBack() handleBack()
@ -127,9 +109,11 @@ internal fun CallScreenView(
requestPermissions(androidPermissions.toTypedArray(), callback) requestPermissions(androidPermissions.toTypedArray(), callback)
}, },
onCreateWebView = { webView -> onCreateWebView = { webView ->
webView.addBackHandler(onBackPressed = ::handleBack)
val interceptor = WebViewWidgetMessageInterceptor( val interceptor = WebViewWidgetMessageInterceptor(
webView = webView, webView = webView,
onUrlLoaded = { url -> onUrlLoaded = { url ->
webView.evaluateJavascript("controls.onBackButtonPressed = () => { backHandler.onBackPressed() }", null)
if (webViewAudioManager?.isInCallMode?.get() == false) { if (webViewAudioManager?.isInCallMode?.get() == false) {
Timber.d("URL $url is loaded, starting in-call audio mode") Timber.d("URL $url is loaded, starting in-call audio mode")
webViewAudioManager?.onCallStarted() webViewAudioManager?.onCallStarted()
@ -282,6 +266,17 @@ private fun WebView.setup(
} }
} }
private fun WebView.addBackHandler(onBackPressed: () -> Unit) {
addJavascriptInterface(
object {
@Suppress("unused")
@JavascriptInterface
fun onBackPressed() = onBackPressed()
},
"backHandler"
)
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun CallScreenViewPreview( internal fun CallScreenViewPreview(
@ -294,18 +289,6 @@ internal fun CallScreenViewPreview(
) )
} }
@PreviewsDayNight
@Composable
internal fun CallScreenPipViewPreview(
@PreviewParameter(PictureInPictureStateProvider::class) state: PictureInPictureState,
) = ElementPreview {
CallScreenView(
state = aCallScreenState(),
pipState = state,
requestPermissions = { _, _ -> },
)
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun InvalidAudioDeviceDialogPreview() = ElementPreview { internal fun InvalidAudioDeviceDialogPreview() = ElementPreview {

View file

@ -1,83 +0,0 @@
/*
* Copyright 2024 New Vector 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.features.call.impl.ui
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.call.impl.pip.PictureInPictureEvents
import io.element.android.features.call.impl.pip.PictureInPictureState
import io.element.android.features.call.impl.pip.aPictureInPictureState
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CallScreenViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back when pip is not supported hangs up`() {
val eventsRecorder = EventsRecorder<CallScreenEvents>()
val pipEventsRecorder = EventsRecorder<PictureInPictureEvents>()
rule.setCallScreenView(
aCallScreenState(
eventSink = eventsRecorder
),
aPictureInPictureState(
supportPip = false,
eventSink = pipEventsRecorder,
),
)
rule.pressBack()
eventsRecorder.assertSize(2)
eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels }
eventsRecorder.assertTrue(1) { it == CallScreenEvents.Hangup }
pipEventsRecorder.assertSize(1)
pipEventsRecorder.assertTrue(0) { it is PictureInPictureEvents.SetPipController }
}
@Test
fun `clicking on back when pip is supported enables PiP`() {
val eventsRecorder = EventsRecorder<CallScreenEvents>()
val pipEventsRecorder = EventsRecorder<PictureInPictureEvents>()
rule.setCallScreenView(
aCallScreenState(
eventSink = eventsRecorder
),
aPictureInPictureState(
supportPip = true,
eventSink = pipEventsRecorder,
),
)
rule.pressBack()
eventsRecorder.assertSize(1)
eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels }
pipEventsRecorder.assertSize(2)
pipEventsRecorder.assertTrue(0) { it is PictureInPictureEvents.SetPipController }
pipEventsRecorder.assertTrue(1) { it == PictureInPictureEvents.EnterPictureInPicture }
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCallScreenView(
state: CallScreenState,
pipState: PictureInPictureState,
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit = { _, _ -> },
) {
setContent {
CallScreenView(
state = state,
pipState = pipState,
requestPermissions = requestPermissions,
)
}
}

View file

@ -18,6 +18,7 @@ import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import org.matrix.rustcomponents.sdk.newVirtualElementCallWidget import org.matrix.rustcomponents.sdk.newVirtualElementCallWidget
import uniffi.matrix_sdk.EncryptionSystem import uniffi.matrix_sdk.EncryptionSystem
import uniffi.matrix_sdk.HeaderStyle
import uniffi.matrix_sdk.VirtualElementCallWidgetOptions import uniffi.matrix_sdk.VirtualElementCallWidgetOptions
import javax.inject.Inject import javax.inject.Inject
import uniffi.matrix_sdk.Intent as CallIntent import uniffi.matrix_sdk.Intent as CallIntent
@ -48,9 +49,10 @@ class DefaultCallWidgetSettingsProvider @Inject constructor(
sentryDsn = callAnalyticsCredentialsProvider.sentryDsn.takeIf { isAnalyticsEnabled }, sentryDsn = callAnalyticsCredentialsProvider.sentryDsn.takeIf { isAnalyticsEnabled },
sentryEnvironment = if (buildMeta.buildType == BuildType.RELEASE) "RELEASE" else "DEBUG", sentryEnvironment = if (buildMeta.buildType == BuildType.RELEASE) "RELEASE" else "DEBUG",
parentUrl = null, parentUrl = null,
// For backwards compatibility, it'll be ignored in recent versions of Element Call
hideHeader = true, hideHeader = true,
controlledMediaDevices = true, controlledMediaDevices = true,
header = null, header = HeaderStyle.APP_BAR,
) )
val rustWidgetSettings = newVirtualElementCallWidget(options) val rustWidgetSettings = newVirtualElementCallWidget(options)
return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings) return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings)

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b61b391f6380189603939badc2ae5912f5cb072200a001ff946af2d52e81b95a
size 12734

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7696eb0f5bf8698d001ea44be6eb2005f414057f9a65703e6d987e8eb75f7f94
size 9441

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c3d14805d85b33ce1f0fdd26eec433db1973924542cd85a3bb0e4514cdcd857d
size 12392

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:146fc2ce4d344c0ea947fe0370e6b8d94c2e724c69c01c2cc3476a756e1f09e4
size 9315

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8cc555b1614f3d9e5181769ec203da84f0fb01bbec9b7edf1ad53944158aea7e oid sha256:7696eb0f5bf8698d001ea44be6eb2005f414057f9a65703e6d987e8eb75f7f94
size 12824 size 9441

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5480b1c7a3244e0c702f6623f736bfc76560843a8eba8c118a1560954414b4f4 oid sha256:5d22e3789c7515c5fcb0ee0c72a7c238947f526b6959e1590a29008c8cfa9919
size 14560 size 11737

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8b42b1ad21ab76ffb7b555809e5d5d4495a6b0faceba181735369e0635ed0a56 oid sha256:b4d802d233fdb12dbbb8136d8b127b10898ba7c5963ebbe2f8ce4d015016b984
size 13847 size 10892

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:73bbf4bfe808722a4d2444ab78bd1f6ab6978d0e427dfaed0d0b957d153a00e5 oid sha256:6d885da406fe9866a5390a65bb6f8dda3e2242bdec4ac59e0ec654a16838bbf6
size 19294 size 16309

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:037a5e384eef63ade207a6a938409897f11efc83f1cbb1d382474bf21dbb54c2 oid sha256:146fc2ce4d344c0ea947fe0370e6b8d94c2e724c69c01c2cc3476a756e1f09e4
size 12524 size 9315

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:548c1d91bfee3a9fe22e881d61c469369885f51f80e267ce9e16c6f4501948fe oid sha256:8f2d4611b2fc7156c855b13d8b390bc9be80b877407f521a8d196434e97650fe
size 13372 size 10519

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5cd1c4e6317f6372c3c341f7e681d82533038a15c2287f584603330b3cf62a40 oid sha256:b58875c87cfdf54fcfd6990cc4c7637d74d85faed1c69bc4e5c671afb827ea4d
size 12316 size 9483

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6de376c3fa6098ba16522181a7b7835660c57c77bfd2c7d8ca75d372c4144676 oid sha256:928fecaa9000e8c0309d4bbbd7742921ec83497cb65868ffda1a2faf0cba9b57
size 17437 size 14599