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 27a5787b13
commit 2f6ce3da33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 33 additions and 153 deletions

View file

@ -7,16 +7,6 @@
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(
supportPip: Boolean = false,
isInPictureInPicture: Boolean = false,

View file

@ -11,6 +11,7 @@ import android.annotation.SuppressLint
import android.util.Log
import android.view.ViewGroup
import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.tooling.preview.PreviewParameter
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.pip.PictureInPictureEvents
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.utils.InvalidAudioDeviceReason
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.libraries.architecture.AsyncData
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.preview.ElementPreview
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.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings
import timber.log.Timber
@ -60,7 +56,6 @@ interface CallScreenNavigator {
fun close()
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun CallScreenView(
state: CallScreenState,
@ -78,19 +73,6 @@ internal fun CallScreenView(
Scaffold(
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 ->
BackHandler {
handleBack()
@ -127,9 +109,11 @@ internal fun CallScreenView(
requestPermissions(androidPermissions.toTypedArray(), callback)
},
onCreateWebView = { webView ->
webView.addBackHandler(onBackPressed = ::handleBack)
val interceptor = WebViewWidgetMessageInterceptor(
webView = webView,
onUrlLoaded = { url ->
webView.evaluateJavascript("controls.onBackButtonPressed = () => { backHandler.onBackPressed() }", null)
if (webViewAudioManager?.isInCallMode?.get() == false) {
Timber.d("URL $url is loaded, starting in-call audio mode")
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
@Composable
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
@Composable
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 org.matrix.rustcomponents.sdk.newVirtualElementCallWidget
import uniffi.matrix_sdk.EncryptionSystem
import uniffi.matrix_sdk.HeaderStyle
import uniffi.matrix_sdk.VirtualElementCallWidgetOptions
import javax.inject.Inject
import uniffi.matrix_sdk.Intent as CallIntent
@ -48,9 +49,10 @@ class DefaultCallWidgetSettingsProvider @Inject constructor(
sentryDsn = callAnalyticsCredentialsProvider.sentryDsn.takeIf { isAnalyticsEnabled },
sentryEnvironment = if (buildMeta.buildType == BuildType.RELEASE) "RELEASE" else "DEBUG",
parentUrl = null,
// For backwards compatibility, it'll be ignored in recent versions of Element Call
hideHeader = true,
controlledMediaDevices = true,
header = null,
header = HeaderStyle.APP_BAR,
)
val rustWidgetSettings = newVirtualElementCallWidget(options)
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
oid sha256:8cc555b1614f3d9e5181769ec203da84f0fb01bbec9b7edf1ad53944158aea7e
size 12824
oid sha256:7696eb0f5bf8698d001ea44be6eb2005f414057f9a65703e6d987e8eb75f7f94
size 9441

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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