Merge branch 'develop' into feature/bma/realDarkTheme
This commit is contained in:
commit
da78222090
21 changed files with 521 additions and 33 deletions
7
.github/pull_request_template.md
vendored
7
.github/pull_request_template.md
vendored
|
|
@ -1,12 +1,5 @@
|
|||
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
|
||||
|
||||
## Type of change
|
||||
|
||||
- [ ] Feature
|
||||
- [ ] Bugfix
|
||||
- [ ] Technical
|
||||
- [ ] Other :
|
||||
|
||||
## Content
|
||||
|
||||
<!-- Describe shortly what has been changed -->
|
||||
|
|
|
|||
|
|
@ -70,4 +70,6 @@ dependencies {
|
|||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,10 +38,11 @@
|
|||
<application>
|
||||
<activity
|
||||
android:name=".ui.ElementCallActivity"
|
||||
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||
android:exported="true"
|
||||
android:label="@string/element_call"
|
||||
android:launchMode="singleTask"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:taskAffinity="io.element.android.features.call">
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
|
|
@ -77,10 +78,11 @@
|
|||
|
||||
</activity>
|
||||
|
||||
<activity android:name=".ui.IncomingCallActivity"
|
||||
<activity
|
||||
android:name=".ui.IncomingCallActivity"
|
||||
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity="io.element.android.features.call" />
|
||||
|
||||
|
|
@ -90,9 +92,10 @@
|
|||
android:exported="false"
|
||||
android:foregroundServiceType="phoneCall" />
|
||||
|
||||
<receiver android:name=".receivers.DeclineCallBroadcastReceiver"
|
||||
android:exported="false"
|
||||
android:enabled="true" />
|
||||
<receiver
|
||||
android:name=".receivers.DeclineCallBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.impl.pip
|
||||
|
||||
sealed interface PictureInPictureEvents {
|
||||
data object EnterPictureInPicture : PictureInPictureEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.impl.pip
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PictureInPictureParams
|
||||
import android.os.Build
|
||||
import android.util.Rational
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("PiP")
|
||||
|
||||
class PictureInPicturePresenter @Inject constructor(
|
||||
pipSupportProvider: PipSupportProvider,
|
||||
) : Presenter<PictureInPictureState> {
|
||||
private val isPipSupported = pipSupportProvider.isPipSupported()
|
||||
private var isInPictureInPicture = mutableStateOf(false)
|
||||
private var hostActivity: WeakReference<Activity>? = null
|
||||
|
||||
@Composable
|
||||
override fun present(): PictureInPictureState {
|
||||
fun handleEvent(event: PictureInPictureEvents) {
|
||||
when (event) {
|
||||
PictureInPictureEvents.EnterPictureInPicture -> switchToPip()
|
||||
}
|
||||
}
|
||||
|
||||
return PictureInPictureState(
|
||||
supportPip = isPipSupported,
|
||||
isInPictureInPicture = isInPictureInPicture.value,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
fun onCreate(activity: Activity) {
|
||||
if (isPipSupported) {
|
||||
Timber.tag(loggerTag.value).d("onCreate: Setting PiP params")
|
||||
hostActivity = WeakReference(activity)
|
||||
hostActivity?.get()?.setPictureInPictureParams(getPictureInPictureParams())
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("onCreate: PiP is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
Timber.tag(loggerTag.value).d("onDestroy")
|
||||
hostActivity?.clear()
|
||||
hostActivity = null
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun getPictureInPictureParams(): PictureInPictureParams {
|
||||
return PictureInPictureParams.Builder()
|
||||
// Portrait for calls seems more appropriate
|
||||
.setAspectRatio(Rational(3, 5))
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
setAutoEnterEnabled(true)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters Picture-in-Picture mode.
|
||||
*/
|
||||
private fun switchToPip() {
|
||||
if (isPipSupported) {
|
||||
Timber.tag(loggerTag.value).d("Switch to PiP mode")
|
||||
hostActivity?.get()?.enterPictureInPictureMode(getPictureInPictureParams())
|
||||
?.also { Timber.tag(loggerTag.value).d("Switch to PiP mode result: $it") }
|
||||
}
|
||||
}
|
||||
|
||||
fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||
Timber.tag(loggerTag.value).d("onPictureInPictureModeChanged: $isInPictureInPictureMode")
|
||||
isInPictureInPicture.value = isInPictureInPictureMode
|
||||
}
|
||||
|
||||
fun onUserLeaveHint() {
|
||||
Timber.tag(loggerTag.value).d("onUserLeaveHint")
|
||||
switchToPip()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.impl.pip
|
||||
|
||||
data class PictureInPictureState(
|
||||
val supportPip: Boolean,
|
||||
val isInPictureInPicture: Boolean,
|
||||
val eventSink: (PictureInPictureEvents) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.impl.pip
|
||||
|
||||
fun aPictureInPictureState(
|
||||
supportPip: Boolean = false,
|
||||
isInPictureInPicture: Boolean = false,
|
||||
eventSink: (PictureInPictureEvents) -> Unit = {},
|
||||
): PictureInPictureState {
|
||||
return PictureInPictureState(
|
||||
supportPip = supportPip,
|
||||
isInPictureInPicture = isInPictureInPicture,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.impl.pip
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
interface PipSupportProvider {
|
||||
@ChecksSdkIntAtLeast(Build.VERSION_CODES.O)
|
||||
fun isPipSupported(): Boolean
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPipSupportProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : PipSupportProvider {
|
||||
override fun isPipSupported(): Boolean {
|
||||
val hasSystemFeaturePip = context.packageManager?.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE).orFalse()
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && hasSystemFeaturePip
|
||||
}
|
||||
}
|
||||
|
|
@ -23,11 +23,12 @@ open class CallScreenStateProvider : PreviewParameterProvider<CallScreenState> {
|
|||
override val values: Sequence<CallScreenState>
|
||||
get() = sequenceOf(
|
||||
aCallScreenState(),
|
||||
aCallScreenState(urlState = AsyncData.Loading()),
|
||||
aCallScreenState(urlState = AsyncData.Failure(Exception("An error occurred"))),
|
||||
)
|
||||
}
|
||||
|
||||
private fun aCallScreenState(
|
||||
internal fun aCallScreenState(
|
||||
urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
|
||||
userAgent: String = "",
|
||||
isInWidgetMode: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ 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.aPictureInPictureState
|
||||
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
|
|
@ -58,25 +61,36 @@ interface CallScreenNavigator {
|
|||
@Composable
|
||||
internal fun CallScreenView(
|
||||
state: CallScreenState,
|
||||
pipState: PictureInPictureState,
|
||||
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun handleBack() {
|
||||
if (pipState.supportPip) {
|
||||
pipState.eventSink.invoke(PictureInPictureEvents.EnterPictureInPicture)
|
||||
} else {
|
||||
state.eventSink(CallScreenEvents.Hangup)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.element_call)) },
|
||||
navigationIcon = {
|
||||
BackButton(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
onClick = { state.eventSink(CallScreenEvents.Hangup) }
|
||||
)
|
||||
}
|
||||
)
|
||||
if (!pipState.isInPictureInPicture) {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.element_call)) },
|
||||
navigationIcon = {
|
||||
BackButton(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
onClick = ::handleBack,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
BackHandler {
|
||||
state.eventSink(CallScreenEvents.Hangup)
|
||||
handleBack()
|
||||
}
|
||||
CallWebView(
|
||||
modifier = Modifier
|
||||
|
|
@ -177,6 +191,7 @@ internal fun CallScreenViewPreview(
|
|||
) = ElementPreview {
|
||||
CallScreenView(
|
||||
state = state,
|
||||
pipState = aPictureInPictureState(),
|
||||
requestPermissions = { _, _ -> },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import io.element.android.compound.theme.mapToTheme
|
|||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
||||
import io.element.android.features.call.impl.di.CallBindings
|
||||
import io.element.android.features.call.impl.pip.PictureInPicturePresenter
|
||||
import io.element.android.features.call.impl.services.CallForegroundService
|
||||
import io.element.android.features.call.impl.utils.CallIntentDataParser
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
|
|
@ -52,6 +53,7 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
|||
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
||||
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
||||
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
||||
@Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter
|
||||
|
||||
private lateinit var presenter: CallScreenPresenter
|
||||
|
||||
|
|
@ -86,6 +88,8 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
|||
updateUiMode(resources.configuration)
|
||||
}
|
||||
|
||||
pictureInPicturePresenter.onCreate(this)
|
||||
|
||||
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
requestAudioFocus()
|
||||
|
||||
|
|
@ -94,12 +98,14 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
|||
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||
}
|
||||
.collectAsState(initial = Theme.System)
|
||||
val pipState = pictureInPicturePresenter.present()
|
||||
ElementTheme(
|
||||
darkTheme = theme.isDark()
|
||||
) {
|
||||
val state = presenter.present()
|
||||
CallScreenView(
|
||||
state = state,
|
||||
pipState = pipState,
|
||||
requestPermissions = { permissions, callback ->
|
||||
requestPermissionCallback = callback
|
||||
requestPermissionsLauncher.launch(permissions)
|
||||
|
|
@ -114,6 +120,11 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
|||
updateUiMode(newConfig)
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
pictureInPicturePresenter.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setCallType(intent)
|
||||
|
|
@ -131,10 +142,16 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
pictureInPicturePresenter.onUserLeaveHint()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
releaseAudioFocus()
|
||||
CallForegroundService.stop(this)
|
||||
pictureInPicturePresenter.onDestroy()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.impl.pip
|
||||
|
||||
class FakePipSupportProvider(
|
||||
private val isPipSupported: Boolean
|
||||
) : PipSupportProvider {
|
||||
override fun isPipSupported() = isPipSupported
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.impl.pip
|
||||
|
||||
import android.os.Build.VERSION_CODES
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.call.impl.ui.ElementCallActivity
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class PictureInPicturePresenterTest {
|
||||
@Test
|
||||
@Config(sdk = [VERSION_CODES.O, VERSION_CODES.S])
|
||||
fun `when pip is not supported, the state value supportPip is false`() = runTest {
|
||||
val presenter = createPictureInPicturePresenter(supportPip = false)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.supportPip).isFalse()
|
||||
}
|
||||
presenter.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [VERSION_CODES.O, VERSION_CODES.S])
|
||||
fun `when pip is supported, the state value supportPip is true`() = runTest {
|
||||
val presenter = createPictureInPicturePresenter(supportPip = true)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.supportPip).isTrue()
|
||||
}
|
||||
presenter.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [VERSION_CODES.S])
|
||||
fun `when entering pip is supported, the state value isInPictureInPicture is true`() = runTest {
|
||||
val presenter = createPictureInPicturePresenter(supportPip = true)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isInPictureInPicture).isFalse()
|
||||
initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture)
|
||||
presenter.onPictureInPictureModeChanged(true)
|
||||
val pipState = awaitItem()
|
||||
assertThat(pipState.isInPictureInPicture).isTrue()
|
||||
// User stops pip
|
||||
presenter.onPictureInPictureModeChanged(false)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.isInPictureInPicture).isFalse()
|
||||
}
|
||||
presenter.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [VERSION_CODES.S])
|
||||
fun `when onUserLeaveHint is called, the state value isInPictureInPicture becomes true`() = runTest {
|
||||
val presenter = createPictureInPicturePresenter(supportPip = true)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isInPictureInPicture).isFalse()
|
||||
presenter.onUserLeaveHint()
|
||||
presenter.onPictureInPictureModeChanged(true)
|
||||
val pipState = awaitItem()
|
||||
assertThat(pipState.isInPictureInPicture).isTrue()
|
||||
}
|
||||
presenter.onDestroy()
|
||||
}
|
||||
|
||||
private fun createPictureInPicturePresenter(
|
||||
supportPip: Boolean = true,
|
||||
): PictureInPicturePresenter {
|
||||
val activity = Robolectric.buildActivity(ElementCallActivity::class.java)
|
||||
return PictureInPicturePresenter(
|
||||
pipSupportProvider = FakePipSupportProvider(supportPip),
|
||||
).apply {
|
||||
onCreate(activity.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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>(expectEvents = false)
|
||||
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 }
|
||||
}
|
||||
|
||||
@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.assertSingle(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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ class BlockedUserViewTest {
|
|||
fun `clicking on back invokes back callback`() {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setLogoutView(
|
||||
rule.setBlockedUsersView(
|
||||
aBlockedUsersState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
|
|
@ -59,7 +59,7 @@ class BlockedUserViewTest {
|
|||
fun `clicking on a user emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||
val userList = aMatrixUserList()
|
||||
rule.setLogoutView(
|
||||
rule.setBlockedUsersView(
|
||||
aBlockedUsersState(
|
||||
blockedUsers = userList,
|
||||
eventSink = eventsRecorder
|
||||
|
|
@ -72,7 +72,7 @@ class BlockedUserViewTest {
|
|||
@Test
|
||||
fun `clicking on cancel sends a BlockedUsersEvents`() {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||
rule.setLogoutView(
|
||||
rule.setBlockedUsersView(
|
||||
aBlockedUsersState(
|
||||
unblockUserAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder
|
||||
|
|
@ -85,7 +85,7 @@ class BlockedUserViewTest {
|
|||
@Test
|
||||
fun `clicking on confirm sends a BlockedUsersEvents`() {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||
rule.setLogoutView(
|
||||
rule.setBlockedUsersView(
|
||||
aBlockedUsersState(
|
||||
unblockUserAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder
|
||||
|
|
@ -96,7 +96,7 @@ class BlockedUserViewTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLogoutView(
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setBlockedUsersView(
|
||||
state: BlockedUsersState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -42,4 +42,12 @@ class EventsRecorder<T>(
|
|||
fun assertList(expectedEvents: List<T>) {
|
||||
assertThat(events).isEqualTo(expectedEvents)
|
||||
}
|
||||
|
||||
fun assertSize(size: Int) {
|
||||
assertThat(events.size).isEqualTo(size)
|
||||
}
|
||||
|
||||
fun assertTrue(index: Int, predicate: (T) -> Boolean) {
|
||||
assertThat(predicate(events[index])).isTrue()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:68d9ca60586aac84157c60f126b17b70ca9d52087da80f253b60f47de87d7ff6
|
||||
size 13750
|
||||
oid sha256:c976f3c1d4809c28cb865b0dfe7ce1eed5fe2c9959a80da8efab5d3594e38e41
|
||||
size 14427
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:68d9ca60586aac84157c60f126b17b70ca9d52087da80f253b60f47de87d7ff6
|
||||
size 13750
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b676c158a1f820d50c9ffd50d048d5c236ee8356279663f257a4882f06c5a1a9
|
||||
size 12214
|
||||
oid sha256:d6acbdb4ea1e66fa4638fc9b454566968081c511e0dcfde3f1e57fd9725a1edb
|
||||
size 13263
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b676c158a1f820d50c9ffd50d048d5c236ee8356279663f257a4882f06c5a1a9
|
||||
size 12214
|
||||
|
|
@ -227,6 +227,7 @@ cp "${fdroidTargetPath}"/app-fdroid-arm64-v8a-release.apk \
|
|||
"${fdroidTargetPath}"/app-fdroid-arm64-v8a-release-signed.apk
|
||||
"${buildToolsPath}"/apksigner sign \
|
||||
-v \
|
||||
--alignment-preserved true \
|
||||
--ks "${keyStorePath}" \
|
||||
--ks-pass pass:"${keyStorePassword}" \
|
||||
--ks-key-alias elementx \
|
||||
|
|
@ -238,6 +239,7 @@ cp "${fdroidTargetPath}"/app-fdroid-armeabi-v7a-release.apk \
|
|||
"${fdroidTargetPath}"/app-fdroid-armeabi-v7a-release-signed.apk
|
||||
"${buildToolsPath}"/apksigner sign \
|
||||
-v \
|
||||
--alignment-preserved true \
|
||||
--ks "${keyStorePath}" \
|
||||
--ks-pass pass:"${keyStorePassword}" \
|
||||
--ks-key-alias elementx \
|
||||
|
|
@ -249,6 +251,7 @@ cp "${fdroidTargetPath}"/app-fdroid-x86-release.apk \
|
|||
"${fdroidTargetPath}"/app-fdroid-x86-release-signed.apk
|
||||
"${buildToolsPath}"/apksigner sign \
|
||||
-v \
|
||||
--alignment-preserved true \
|
||||
--ks "${keyStorePath}" \
|
||||
--ks-pass pass:"${keyStorePassword}" \
|
||||
--ks-key-alias elementx \
|
||||
|
|
@ -260,6 +263,7 @@ cp "${fdroidTargetPath}"/app-fdroid-x86_64-release.apk \
|
|||
"${fdroidTargetPath}"/app-fdroid-x86_64-release-signed.apk
|
||||
"${buildToolsPath}"/apksigner sign \
|
||||
-v \
|
||||
--alignment-preserved true \
|
||||
--ks "${keyStorePath}" \
|
||||
--ks-pass pass:"${keyStorePassword}" \
|
||||
--ks-key-alias elementx \
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue