* Revert "Change native back button behavior in EC view (close settings in EC with os native back) (#6642)"
This reverts commit 79eb28d10a.
* Fix API breaks from revert
This commit is contained in:
parent
4eb2481e71
commit
6abb83547b
4 changed files with 10 additions and 296 deletions
|
|
@ -1,26 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.features.call.impl.ui
|
|
||||||
internal sealed interface CallScreenBackPressAction {
|
|
||||||
data object DispatchEscapeToWebView : CallScreenBackPressAction
|
|
||||||
data object EnterPictureInPicture : CallScreenBackPressAction
|
|
||||||
}
|
|
||||||
|
|
||||||
internal object CallScreenBackPressPolicy {
|
|
||||||
fun resolve(
|
|
||||||
supportPip: Boolean,
|
|
||||||
hasWebView: Boolean,
|
|
||||||
fromNative: Boolean,
|
|
||||||
): CallScreenBackPressAction? {
|
|
||||||
return when {
|
|
||||||
hasWebView && fromNative -> CallScreenBackPressAction.DispatchEscapeToWebView
|
|
||||||
hasWebView && supportPip -> CallScreenBackPressAction.EnterPictureInPicture
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -64,15 +64,11 @@ internal fun CallScreenView(
|
||||||
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
|
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var callWebView by remember { mutableStateOf<WebView?>(null) }
|
fun handleBack() {
|
||||||
|
if (pipState.supportPip) {
|
||||||
fun handleBack(fromNative: Boolean = false) {
|
pipState.eventSink.invoke(PictureInPictureEvent.EnterPictureInPicture)
|
||||||
when (CallScreenBackPressPolicy.resolve(supportPip = pipState.supportPip, hasWebView = callWebView != null, fromNative)) {
|
} else {
|
||||||
CallScreenBackPressAction.EnterPictureInPicture ->
|
state.eventSink(CallScreenEvent.Hangup)
|
||||||
pipState.eventSink(PictureInPictureEvent.EnterPictureInPicture)
|
|
||||||
CallScreenBackPressAction.DispatchEscapeToWebView ->
|
|
||||||
callWebView?.dispatchEscKeyEvent()
|
|
||||||
null -> Timber.d("Back press with unsupported pip is a no-op")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,7 +76,7 @@ internal fun CallScreenView(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) { padding ->
|
) { padding ->
|
||||||
BackHandler {
|
BackHandler {
|
||||||
handleBack(fromNative = true)
|
handleBack()
|
||||||
}
|
}
|
||||||
if (state.webViewError != null) {
|
if (state.webViewError != null) {
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
|
|
@ -115,7 +111,6 @@ internal fun CallScreenView(
|
||||||
},
|
},
|
||||||
onConsoleMessage = onConsoleMessage,
|
onConsoleMessage = onConsoleMessage,
|
||||||
onCreateWebView = { webView ->
|
onCreateWebView = { webView ->
|
||||||
callWebView = webView
|
|
||||||
webView.addBackHandler(onBackPressed = ::handleBack)
|
webView.addBackHandler(onBackPressed = ::handleBack)
|
||||||
val interceptor = WebViewWidgetMessageInterceptor(
|
val interceptor = WebViewWidgetMessageInterceptor(
|
||||||
webView = webView,
|
webView = webView,
|
||||||
|
|
@ -140,7 +135,6 @@ internal fun CallScreenView(
|
||||||
pipState.eventSink(PictureInPictureEvent.SetPipController(pipController))
|
pipState.eventSink(PictureInPictureEvent.SetPipController(pipController))
|
||||||
},
|
},
|
||||||
onDestroyWebView = {
|
onDestroyWebView = {
|
||||||
callWebView = null
|
|
||||||
// Reset audio mode
|
// Reset audio mode
|
||||||
webViewAudioManager?.onCallStopped()
|
webViewAudioManager?.onCallStopped()
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +143,6 @@ internal fun CallScreenView(
|
||||||
AsyncData.Uninitialized,
|
AsyncData.Uninitialized,
|
||||||
is AsyncData.Loading ->
|
is AsyncData.Loading ->
|
||||||
ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait))
|
ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait))
|
||||||
|
|
||||||
is AsyncData.Failure -> {
|
is AsyncData.Failure -> {
|
||||||
Timber.e(state.urlState.error, "WebView failed to load URL: ${state.urlState.error.message}")
|
Timber.e(state.urlState.error, "WebView failed to load URL: ${state.urlState.error.message}")
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
|
|
@ -157,7 +150,6 @@ internal fun CallScreenView(
|
||||||
onSubmit = { state.eventSink(CallScreenEvent.Hangup) },
|
onSubmit = { state.eventSink(CallScreenEvent.Hangup) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AsyncData.Success -> Unit
|
is AsyncData.Success -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -256,18 +248,15 @@ private fun WebView.setup(
|
||||||
|
|
||||||
private fun WebView.addBackHandler(onBackPressed: () -> Unit) {
|
private fun WebView.addBackHandler(onBackPressed: () -> Unit) {
|
||||||
addJavascriptInterface(
|
addJavascriptInterface(
|
||||||
JavascriptBackHandler {
|
object {
|
||||||
onBackPressed()
|
@Suppress("unused")
|
||||||
|
@JavascriptInterface
|
||||||
|
fun onBackPressed() = onBackPressed()
|
||||||
},
|
},
|
||||||
"backHandler"
|
"backHandler"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun WebView.dispatchEscKeyEvent() {
|
|
||||||
dispatchKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_ESCAPE))
|
|
||||||
dispatchKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_ESCAPE))
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CallScreenViewPreview(
|
internal fun CallScreenViewPreview(
|
||||||
|
|
@ -286,8 +275,3 @@ internal fun CallScreenViewPreview(
|
||||||
internal fun InvalidAudioDeviceDialogPreview() = ElementPreview {
|
internal fun InvalidAudioDeviceDialogPreview() = ElementPreview {
|
||||||
InvalidAudioDeviceDialog(invalidAudioDeviceReason = InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED) {}
|
InvalidAudioDeviceDialog(invalidAudioDeviceReason = InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun interface JavascriptBackHandler {
|
|
||||||
@JavascriptInterface
|
|
||||||
fun onBackPressed()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.features.call.ui
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.features.call.impl.ui.CallScreenBackPressAction
|
|
||||||
import io.element.android.features.call.impl.ui.CallScreenBackPressPolicy
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class CallScreenBackPressPolicyTest {
|
|
||||||
@Test
|
|
||||||
fun `resolve returns dispatch escape when a web view is available and native button is pressed`() {
|
|
||||||
val result = CallScreenBackPressPolicy.resolve(
|
|
||||||
supportPip = false,
|
|
||||||
hasWebView = true,
|
|
||||||
fromNative = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(result).isEqualTo(CallScreenBackPressAction.DispatchEscapeToWebView)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `resolve dispatch escape when there is a web view and pip is supported on native button press`() {
|
|
||||||
val result = CallScreenBackPressPolicy.resolve(
|
|
||||||
supportPip = true,
|
|
||||||
hasWebView = true,
|
|
||||||
fromNative = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(result).isEqualTo(CallScreenBackPressAction.DispatchEscapeToWebView)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `resolve returns hangup when there is no web view and pip is not supported from native button`() {
|
|
||||||
val result = CallScreenBackPressPolicy.resolve(
|
|
||||||
supportPip = false,
|
|
||||||
hasWebView = false,
|
|
||||||
fromNative = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(result).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `resolve returns hangup when there is no web view even though pip is supported from native button`() {
|
|
||||||
val result = CallScreenBackPressPolicy.resolve(
|
|
||||||
supportPip = true,
|
|
||||||
hasWebView = false,
|
|
||||||
fromNative = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(result).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `resolve goes to pip if its not from native but from the webview`() {
|
|
||||||
val result = CallScreenBackPressPolicy.resolve(
|
|
||||||
supportPip = true,
|
|
||||||
hasWebView = true,
|
|
||||||
fromNative = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(result).isEqualTo(CallScreenBackPressAction.EnterPictureInPicture)
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
fun `resolve hangs up if its not from native but from the webview and pip is not supported`() {
|
|
||||||
val result = CallScreenBackPressPolicy.resolve(
|
|
||||||
supportPip = false,
|
|
||||||
hasWebView = true,
|
|
||||||
fromNative = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(result).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `invalid cases (event comes from webview but there is now webview) all result in hangup`() {
|
|
||||||
val withPipSupport = CallScreenBackPressPolicy.resolve(
|
|
||||||
supportPip = true,
|
|
||||||
hasWebView = false,
|
|
||||||
fromNative = false,
|
|
||||||
)
|
|
||||||
assertThat(withPipSupport).isNull()
|
|
||||||
val withOutPipSupport = CallScreenBackPressPolicy.resolve(
|
|
||||||
supportPip = false,
|
|
||||||
hasWebView = false,
|
|
||||||
fromNative = false,
|
|
||||||
)
|
|
||||||
assertThat(withOutPipSupport).isNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalTestApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.call.ui
|
|
||||||
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.webkit.WebView
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
|
||||||
import androidx.compose.ui.test.AndroidComposeUiTest
|
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
|
||||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import io.element.android.features.call.impl.pip.PictureInPictureEvent
|
|
||||||
import io.element.android.features.call.impl.pip.aPictureInPictureState
|
|
||||||
import io.element.android.features.call.impl.ui.CallScreenEvent
|
|
||||||
import io.element.android.features.call.impl.ui.CallScreenView
|
|
||||||
import io.element.android.features.call.impl.ui.JavascriptBackHandler
|
|
||||||
import io.element.android.features.call.impl.ui.aCallScreenState
|
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
|
||||||
import io.element.android.tests.testutils.pressBackKey
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.robolectric.annotation.Config
|
|
||||||
import org.robolectric.annotation.Implementation
|
|
||||||
import org.robolectric.annotation.Implements
|
|
||||||
import org.robolectric.annotation.Resetter
|
|
||||||
import org.robolectric.shadows.ShadowWebView
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class CallScreenViewTest {
|
|
||||||
@Test
|
|
||||||
fun `pressing back key triggers hangup when no web view is available and pip is unsupported`() = runAndroidComposeUiTest {
|
|
||||||
val callEvents = EventsRecorder<CallScreenEvent>()
|
|
||||||
|
|
||||||
setCallScreenView(
|
|
||||||
state = aCallScreenState(eventSink = callEvents),
|
|
||||||
useInspectionMode = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
pressBackKey()
|
|
||||||
|
|
||||||
callEvents.assertEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Config(shadows = [RecordingShadowWebView::class])
|
|
||||||
@Test
|
|
||||||
fun `pressing back key dispatches escape key events to web view when pip is unsupported`() = runAndroidComposeUiTest {
|
|
||||||
setCallScreenView(
|
|
||||||
state = aCallScreenState(),
|
|
||||||
useInspectionMode = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
pressBackKey()
|
|
||||||
|
|
||||||
val dispatchedEvents = RecordingShadowWebView.dispatchedEvents
|
|
||||||
assertEquals(2, dispatchedEvents.size)
|
|
||||||
assertEquals(KeyEvent.ACTION_DOWN, dispatchedEvents[0].action)
|
|
||||||
assertEquals(KeyEvent.KEYCODE_ESCAPE, dispatchedEvents[0].keyCode)
|
|
||||||
assertEquals(KeyEvent.ACTION_UP, dispatchedEvents[1].action)
|
|
||||||
assertEquals(KeyEvent.KEYCODE_ESCAPE, dispatchedEvents[1].keyCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Config(shadows = [RecordingShadowWebView::class])
|
|
||||||
@Test
|
|
||||||
fun `web view javascript back handler emits pip event when pip is supported`() = runAndroidComposeUiTest {
|
|
||||||
val pipEvents = EventsRecorder<PictureInPictureEvent>()
|
|
||||||
|
|
||||||
setCallScreenView(
|
|
||||||
state = aCallScreenState(),
|
|
||||||
useInspectionMode = false,
|
|
||||||
pipState = aPictureInPictureState(
|
|
||||||
supportPip = true,
|
|
||||||
eventSink = pipEvents,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
runOnIdle {
|
|
||||||
RecordingShadowWebView.invokeJavascriptBackHandler()
|
|
||||||
}
|
|
||||||
|
|
||||||
pipEvents.assertSize(2)
|
|
||||||
pipEvents.assertTrue(0) { it is PictureInPictureEvent.SetPipController }
|
|
||||||
pipEvents.assertTrue(1) { it is PictureInPictureEvent.EnterPictureInPicture }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun AndroidComposeUiTest<ComponentActivity>.setCallScreenView(
|
|
||||||
state: io.element.android.features.call.impl.ui.CallScreenState,
|
|
||||||
useInspectionMode: Boolean,
|
|
||||||
pipState: io.element.android.features.call.impl.pip.PictureInPictureState = aPictureInPictureState(supportPip = false),
|
|
||||||
) {
|
|
||||||
setContent {
|
|
||||||
// Inspection mode disables AndroidView creation; keep it configurable per test.
|
|
||||||
CompositionLocalProvider(LocalInspectionMode provides useInspectionMode) {
|
|
||||||
CallScreenView(
|
|
||||||
state = state,
|
|
||||||
pipState = pipState,
|
|
||||||
onConsoleMessage = {},
|
|
||||||
requestPermissions = { _, _ -> },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Implements(WebView::class)
|
|
||||||
internal class RecordingShadowWebView : ShadowWebView() {
|
|
||||||
companion object {
|
|
||||||
val dispatchedEvents = mutableListOf<KeyEvent>()
|
|
||||||
private var backHandlerJavascriptInterface: JavascriptBackHandler? = null
|
|
||||||
|
|
||||||
@Resetter
|
|
||||||
@JvmStatic
|
|
||||||
@Suppress("unused")
|
|
||||||
fun resetRecordedEvents() {
|
|
||||||
dispatchedEvents.clear()
|
|
||||||
backHandlerJavascriptInterface = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invokeJavascriptBackHandler() {
|
|
||||||
val backHandler = checkNotNull(backHandlerJavascriptInterface) { "Expected backHandler JavaScript interface to be registered" }
|
|
||||||
backHandler.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Implementation
|
|
||||||
protected override fun addJavascriptInterface(`object`: Any, name: String) {
|
|
||||||
super.addJavascriptInterface(`object`, name)
|
|
||||||
if (name == "backHandler") {
|
|
||||||
backHandlerJavascriptInterface = `object` as? JavascriptBackHandler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Implementation
|
|
||||||
@Suppress("unused")
|
|
||||||
fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
|
||||||
dispatchedEvents += KeyEvent(event)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue