This commit is contained in:
Pavel Prystinka 2024-02-19 04:57:26 -08:00
Родитель bcb36bc87c
Коммит d642737c1f
375 изменённых файлов: 14350 добавлений и 12103 удалений

Просмотреть файл

@ -0,0 +1,8 @@
root = true
[*.{kt,kts}]
ktlint_standard_max-line-length = disabled
ktlint_standard_property-naming = disabled
ktlint_standard_no-consecutive-comments = disabled
ktlint_standard_filename = disabled
ktlint_standard_blank-line-before-declaration = disabled

Просмотреть файл

@ -95,10 +95,10 @@ buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.2'
// classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jacoco:org.jacoco.core:$jacoco_version"
// NOTE: Do not place your application dependencies here; they belong
@ -108,7 +108,7 @@ buildscript {
plugins {
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
id "org.jlleitschuh.gradle.ktlint" version "10.2.1" apply(true)
id "org.jlleitschuh.gradle.ktlint" version "12.1.0"
}
allprojects {
@ -140,10 +140,10 @@ allprojects {
}
}
}
apply plugin: "org.jlleitschuh.gradle.ktlint"
}
subprojects {
apply plugin: "org.jlleitschuh.gradle.ktlint"
apply from: "../checkstyle.gradle"
afterEvaluate {
preBuild.dependsOn 'checkstyle'

Просмотреть файл

@ -82,13 +82,12 @@ android {
}
ktlint {
debug.set(false)
debug = true
verbose.set(true)
android.set(true)
outputToConsole.set(true)
outputColorName.set("RED")
enableExperimentalRules.set(false)
disabledRules = ["import-ordering", "max-line-length", "parameter-list-wrapping"]
reporters {
reporter "checkstyle"
reporter "plain"

Просмотреть файл

@ -40,7 +40,8 @@ internal open class BaseUiTest {
internal val upperMessageBarNotificationId = R.id.azure_communication_ui_calling_upper_message_bar_notification
internal val upperMessageBarNotificationIconId = R.id.azure_communication_ui_calling_upper_message_bar_notification_icon
internal val upperMessageBarNotificationMessageId = R.id.azure_communication_ui_calling_upper_message_bar_notification_message
internal val upperMessageBarNotificationDismissButtonId = R.id.azure_communication_ui_calling_upper_message_bar_notification_dismiss_button
internal val upperMessageBarNotificationDismissButtonId =
R.id.azure_communication_ui_calling_upper_message_bar_notification_dismiss_button
internal val setupCameraButtonId = R.id.azure_communication_ui_setup_camera_button
internal val callCameraButtonId = R.id.azure_communication_ui_call_switch_camera_button
@ -50,17 +51,19 @@ internal open class BaseUiTest {
internal val userMessageEditTextId = R.id.azure_communication_ui_user_message_edit_text
internal val showSupportFormTextId = R.string.azure_communication_ui_calling_report_issue_title
@Rule
@JvmField
var grantPermissionRule: GrantPermissionRule
private val basePermissionList = arrayOf(
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.WAKE_LOCK",
"android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO"
)
private val basePermissionList =
arrayOf(
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.WAKE_LOCK",
"android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO",
)
init {
grantPermissionRule = GrantPermissionRule.grant(*basePermissionList)

Просмотреть файл

@ -26,48 +26,51 @@ internal fun waitUntilDisplayed(id: Int) {
internal fun assertDisplayed(id: Int): ViewInteraction {
return Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(id)
)
ViewMatchers.withId(id),
),
).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
}
internal fun assertNotDisplayed(id: Int): ViewInteraction? {
return Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(id)
)
ViewMatchers.withId(id),
),
).check(doesNotExist())
}
internal fun assertViewGone(id: Int): ViewInteraction? {
return Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(id)
)
ViewMatchers.withId(id),
),
).check(ViewAssertions.matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
}
internal fun assertViewNotDisplayed(id: Int) {
Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(id)
)
ViewMatchers.withId(id),
),
).check(ViewAssertions.matches(Matchers.not(ViewMatchers.isDisplayed())))
}
internal fun assertNotExist(id: Int): ViewInteraction? {
return Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(id)
)
ViewMatchers.withId(id),
),
).check(doesNotExist())
}
internal fun assertViewText(id: Int, text: String) {
internal fun assertViewText(
id: Int,
text: String,
) {
Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(id)
)
ViewMatchers.withId(id),
),
).check(ViewAssertions.matches(ViewMatchers.withText(text)))
}
@ -75,24 +78,28 @@ internal fun tapOnScreen() {
Espresso.onView(ViewMatchers.isRoot())
.perform(ViewActions.click())
}
internal fun tapWithTextWhenDisplayed(text: String) {
// wait until text is displayed
waitUntilViewIsDisplayed {
Espresso.onView(
Matchers.allOf(ViewMatchers.withText(text), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
Matchers.allOf(ViewMatchers.withText(text), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)),
).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
}
tapDelay()
Espresso.onView(
Matchers.allOf(ViewMatchers.withText(text), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
Matchers.allOf(ViewMatchers.withText(text), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)),
).perform(ViewActions.click())
}
internal fun assertViewText(id: Int, textId: Int) {
internal fun assertViewText(
id: Int,
textId: Int,
) {
Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(id)
)
ViewMatchers.withId(id),
),
).check(ViewAssertions.matches(ViewMatchers.withText(textId)))
}
@ -132,14 +139,19 @@ internal fun waitUntilViewIsDisplayed(idlingCheck: () -> ViewInteraction): ViewI
if (ex is AssertionFailedError || ex is NoMatchingViewException) {
SystemClock.sleep(2000L)
timeOut += 2000L
} else throw ex
} else {
throw ex
}
}
}
if (isReady) return viewInteraction
throw IllegalStateException("Timed out waiting for view")
}
internal fun assertViewHasChild(@IdRes id: Int, n: Int) {
internal fun assertViewHasChild(
@IdRes id: Int,
n: Int,
) {
Espresso.onView(ViewMatchers.withId(id))
.check(ViewAssertions.matches(ViewMatchers.hasChildCount(n)))
}

Просмотреть файл

@ -4,15 +4,15 @@
package com.azure.android.communication.mocking
import androidx.annotation.GuardedBy
import com.azure.android.communication.calling.CameraFacing
import com.azure.android.communication.calling.VideoDeviceType
import com.azure.android.communication.calling.ParticipantState
import com.azure.android.communication.calling.MediaStreamType
import com.azure.android.communication.calling.CallState
import com.azure.android.communication.calling.RemoteVideoStreamsUpdatedListener
import com.azure.android.communication.calling.CameraFacing
import com.azure.android.communication.calling.MediaStreamType
import com.azure.android.communication.calling.ParticipantState
import com.azure.android.communication.calling.PropertyChangedListener
import com.azure.android.communication.ui.calling.models.CallCompositeLobbyErrorCode
import com.azure.android.communication.calling.RemoteVideoStreamsUpdatedListener
import com.azure.android.communication.calling.VideoDeviceType
import com.azure.android.communication.ui.calling.models.CallCompositeInternalParticipantRole
import com.azure.android.communication.ui.calling.models.CallCompositeLobbyErrorCode
import com.azure.android.communication.ui.calling.models.CallDiagnosticQuality
import com.azure.android.communication.ui.calling.models.MediaCallDiagnostic
import com.azure.android.communication.ui.calling.models.MediaCallDiagnosticModel
@ -29,22 +29,22 @@ import com.azure.android.communication.ui.calling.service.sdk.CallingSDK
import com.azure.android.communication.ui.calling.service.sdk.CallingStateWrapper
import com.azure.android.communication.ui.calling.service.sdk.CommunicationIdentifier
import com.azure.android.communication.ui.calling.service.sdk.DominantSpeakersInfo
import com.azure.android.communication.ui.calling.service.sdk.into
import com.azure.android.communication.ui.calling.service.sdk.LocalVideoStream
import com.azure.android.communication.ui.calling.service.sdk.VideoDeviceInfo
import com.azure.android.communication.ui.calling.service.sdk.RemoteVideoStream
import com.azure.android.communication.ui.calling.service.sdk.RemoteParticipant
import com.azure.android.communication.ui.calling.service.sdk.RemoteVideoStream
import com.azure.android.communication.ui.calling.service.sdk.VideoDeviceInfo
import com.azure.android.communication.ui.calling.service.sdk.into
import com.azure.android.communication.ui.calling.utilities.CoroutineContextProvider
import java9.util.concurrent.CompletableFuture
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
internal interface LocalStreamEventObserver {
fun onSwitchSource(deviceInfo: VideoDeviceInfo)
@ -61,7 +61,7 @@ internal class CallEvents {
internal class LocalVideoStreamTest(
private val callEvents: CallEvents,
private val cameraFacing: CameraFacing,
private val coroutineScope: CoroutineScope
private val coroutineScope: CoroutineScope,
) : LocalVideoStream {
override val native: Any = 1
override val source: VideoDeviceInfo
@ -104,22 +104,24 @@ internal class TestCallingSDK(private val callEvents: CallEvents, coroutineConte
state: ParticipantState = ParticipantState.CONNECTED,
isMuted: Boolean = true,
isSpeaking: Boolean = false,
videoStreams: List<MediaStreamType>? = listOf()
videoStreams: List<MediaStreamType>? = listOf(),
) {
val rpi = RemoteParticipantImpl(
identifier = id,
displayName = displayName,
isMuted = isMuted,
isSpeaking = isSpeaking,
videoStreams = videoStreams?.mapIndexed { index, type ->
RemoteVideoStreamImpl(
native = 1,
id = index,
mediaStreamType = type
)
} ?: listOf(),
state = state
)
val rpi =
RemoteParticipantImpl(
identifier = id,
displayName = displayName,
isMuted = isMuted,
isSpeaking = isSpeaking,
videoStreams =
videoStreams?.mapIndexed { index, type ->
RemoteVideoStreamImpl(
native = 1,
id = index,
mediaStreamType = type,
)
} ?: listOf(),
state = state,
)
synchronized(this) {
remoteParticipantsMap[id.id] = rpi
}
@ -127,20 +129,24 @@ internal class TestCallingSDK(private val callEvents: CallEvents, coroutineConte
emitRemoteParticipantFlow()
}
suspend fun changeParticipantState(id: String, state: ParticipantState) {
suspend fun changeParticipantState(
id: String,
state: ParticipantState,
) {
synchronized(this) {
if (!remoteParticipantsMap.containsKey(id)) {
return
}
val rpi = remoteParticipantsMap[id]!!
remoteParticipantsMap[id] = RemoteParticipantImpl(
identifier = rpi.identifier,
displayName = rpi.displayName,
isMuted = rpi.isMuted,
isSpeaking = rpi.isSpeaking,
videoStreams = rpi.videoStreams,
state = state
)
remoteParticipantsMap[id] =
RemoteParticipantImpl(
identifier = rpi.identifier,
displayName = rpi.displayName,
isMuted = rpi.isMuted,
isSpeaking = rpi.isSpeaking,
videoStreams = rpi.videoStreams,
state = state,
)
}
emitRemoteParticipantFlow()
}
@ -166,37 +172,50 @@ internal class TestCallingSDK(private val callEvents: CallEvents, coroutineConte
id: String,
isMuted: Boolean? = null,
isSpeaking: Boolean? = null,
state: ParticipantState? = null
state: ParticipantState? = null,
) {
synchronized(this) {
if (!remoteParticipantsMap.containsKey(id)) {
return
}
val rpi = remoteParticipantsMap[id]!!
remoteParticipantsMap[id] = RemoteParticipantImpl(
identifier = rpi.identifier,
displayName = rpi.displayName,
isMuted = isMuted ?: rpi.isMuted,
isSpeaking = isSpeaking ?: rpi.isSpeaking,
videoStreams = rpi.videoStreams,
state = state ?: rpi.state
)
remoteParticipantsMap[id] =
RemoteParticipantImpl(
identifier = rpi.identifier,
displayName = rpi.displayName,
isMuted = isMuted ?: rpi.isMuted,
isSpeaking = isSpeaking ?: rpi.isSpeaking,
videoStreams = rpi.videoStreams,
state = state ?: rpi.state,
)
}
emitRemoteParticipantFlow()
}
suspend fun setLowNetworkRecieveQuality(lowNetworkReceiveQuality: Boolean) {
val model = NetworkQualityCallDiagnosticModel(NetworkCallDiagnostic.NETWORK_RECEIVE_QUALITY, if (lowNetworkReceiveQuality) CallDiagnosticQuality.BAD else CallDiagnosticQuality.GOOD)
val model =
NetworkQualityCallDiagnosticModel(
NetworkCallDiagnostic.NETWORK_RECEIVE_QUALITY,
if (lowNetworkReceiveQuality) CallDiagnosticQuality.BAD else CallDiagnosticQuality.GOOD,
)
networkQualityCallDiagnosticSharedFlow.emit(model)
}
suspend fun setLowNetworkSendQuality(lowNetworkSendQuality: Boolean) {
val model = NetworkQualityCallDiagnosticModel(NetworkCallDiagnostic.NETWORK_SEND_QUALITY, if (lowNetworkSendQuality) CallDiagnosticQuality.BAD else CallDiagnosticQuality.GOOD)
val model =
NetworkQualityCallDiagnosticModel(
NetworkCallDiagnostic.NETWORK_SEND_QUALITY,
if (lowNetworkSendQuality) CallDiagnosticQuality.BAD else CallDiagnosticQuality.GOOD,
)
networkQualityCallDiagnosticSharedFlow.emit(model)
}
suspend fun setLowNetworkReconnectionQuality(lowNetworkReconnectionQuality: Boolean) {
val model = NetworkQualityCallDiagnosticModel(NetworkCallDiagnostic.NETWORK_RECONNECTION_QUALITY, if (lowNetworkReconnectionQuality) CallDiagnosticQuality.BAD else CallDiagnosticQuality.GOOD)
val model =
NetworkQualityCallDiagnosticModel(
NetworkCallDiagnostic.NETWORK_RECONNECTION_QUALITY,
if (lowNetworkReconnectionQuality) CallDiagnosticQuality.BAD else CallDiagnosticQuality.GOOD,
)
networkQualityCallDiagnosticSharedFlow.emit(model)
}
@ -253,6 +272,7 @@ internal class TestCallingSDK(private val callEvents: CallEvents, coroutineConte
override fun setupCall(): CompletableFuture<Void> {
return completedNullFuture()
}
override fun dispose() {}
override fun turnOnVideoAsync(): CompletableFuture<LocalVideoStream> {
@ -276,11 +296,12 @@ internal class TestCallingSDK(private val callEvents: CallEvents, coroutineConte
}
override fun switchCameraAsync(): CompletableFuture<CameraDeviceSelectionStatus> {
localCameraFacing = when (localCameraFacing) {
CameraFacing.FRONT -> CameraFacing.BACK
CameraFacing.BACK -> CameraFacing.FRONT
else -> TODO("Camera modes that aren't Front or Back not yet implemented")
}
localCameraFacing =
when (localCameraFacing) {
CameraFacing.FRONT -> CameraFacing.BACK
CameraFacing.BACK -> CameraFacing.FRONT
else -> TODO("Camera modes that aren't Front or Back not yet implemented")
}
localVideoStream.switchSource(localVideoStream.source.copy(cameraFacing = localCameraFacing)).join()
@ -371,6 +392,7 @@ internal class TestCallingSDK(private val callEvents: CallEvents, coroutineConte
}
override fun getCamerasCountStateFlow(): StateFlow<Int> = getCameraCountStateFlow
override fun admitAll(): CompletableFuture<CallCompositeLobbyErrorCode?> {
return lobbyResultCompletableFuture
}
@ -409,7 +431,7 @@ internal class TestCallingSDK(private val callEvents: CallEvents, coroutineConte
when (this.mediaStreamType) {
MediaStreamType.VIDEO -> StreamType.VIDEO
MediaStreamType.SCREEN_SHARING -> StreamType.SCREEN_SHARING
}
},
)
}
@ -417,7 +439,7 @@ internal class TestCallingSDK(private val callEvents: CallEvents, coroutineConte
remoteParticipantsInfoModelSharedFlow.emit(
synchronized(this) {
this.getRemoteParticipantsMap().mapValues { it.value.asParticipantInfoModel() }
}
},
)
}
@ -428,16 +450,16 @@ internal class TestCallingSDK(private val callEvents: CallEvents, coroutineConte
isMuted = this.isMuted,
isSpeaking = this.isSpeaking,
participantStatus = this.state.into(),
screenShareVideoStreamModel = this.videoStreams.find {
it.mediaStreamType == MediaStreamType.SCREEN_SHARING
}?.asVideoStreamModel(),
cameraVideoStreamModel = this.videoStreams.find {
it.mediaStreamType == MediaStreamType.VIDEO
}?.asVideoStreamModel(),
screenShareVideoStreamModel =
this.videoStreams.find {
it.mediaStreamType == MediaStreamType.SCREEN_SHARING
}?.asVideoStreamModel(),
cameraVideoStreamModel =
this.videoStreams.find {
it.mediaStreamType == MediaStreamType.VIDEO
}?.asVideoStreamModel(),
modifiedTimestamp = System.currentTimeMillis(),
isCameraDisabled = false
isCameraDisabled = false,
)
}
}
@ -454,7 +476,10 @@ internal fun completedNullFuture(): CompletableFuture<Void> {
return CompletableFuture<Void>().also { it.complete(null) }
}
internal fun completedNullFuture(coroutineScope: CoroutineScope, f: suspend () -> Any): CompletableFuture<Void> {
internal fun completedNullFuture(
coroutineScope: CoroutineScope,
f: suspend () -> Any,
): CompletableFuture<Void> {
val future = CompletableFuture<Void>()
coroutineScope.launch {
f.invoke()

Просмотреть файл

@ -12,12 +12,12 @@ import com.azure.android.communication.calling.CreateViewOptions
import com.azure.android.communication.calling.MediaStreamType
import com.azure.android.communication.calling.ScalingMode
import com.azure.android.communication.ui.calling.presentation.VideoStreamRendererFactory
import com.azure.android.communication.ui.calling.service.sdk.RemoteVideoStream
import com.azure.android.communication.ui.calling.service.sdk.VideoStreamRenderer
import com.azure.android.communication.ui.calling.service.sdk.LocalVideoStream
import com.azure.android.communication.ui.calling.service.sdk.VideoStreamRendererView
import com.azure.android.communication.ui.calling.service.sdk.RemoteVideoStream
import com.azure.android.communication.ui.calling.service.sdk.StreamSize
import com.azure.android.communication.ui.calling.service.sdk.VideoDeviceInfo
import com.azure.android.communication.ui.calling.service.sdk.VideoStreamRenderer
import com.azure.android.communication.ui.calling.service.sdk.VideoStreamRendererView
import com.google.android.apps.common.testing.accessibility.framework.replacements.LayoutParams
internal class TestVideoStreamRendererFactory(private val callEvents: CallEvents) :
@ -44,37 +44,40 @@ internal enum class VideoType {
REMOTE_SCREEN,
LOCAL_VIDEO_FRONT,
LOCAL_VIDEO_BACK,
LOCAL_SCREEN_SHARE
LOCAL_SCREEN_SHARE,
}
internal sealed class Stream {
data class Local(val stream: LocalVideoStream) : Stream()
data class Remote(val stream: RemoteVideoStream) : Stream()
}
internal class TestVideoStreamRendererLocalWrapper(
private val context: Context,
callEvents: CallEvents,
stream: Stream
stream: Stream,
) : VideoStreamRenderer, LocalStreamEventObserver {
private val videoType: VideoType
init {
when (stream) {
is Stream.Local -> {
videoType = when (stream.stream.source.cameraFacing) {
CameraFacing.FRONT -> VideoType.LOCAL_VIDEO_FRONT
CameraFacing.BACK -> VideoType.LOCAL_VIDEO_BACK
else -> TODO("Camera modes that aren't Front or Back not yet implemented")
}
videoType =
when (stream.stream.source.cameraFacing) {
CameraFacing.FRONT -> VideoType.LOCAL_VIDEO_FRONT
CameraFacing.BACK -> VideoType.LOCAL_VIDEO_BACK
else -> TODO("Camera modes that aren't Front or Back not yet implemented")
}
callEvents.localStreamObservers[stream.stream] = this
}
is Stream.Remote -> {
videoType = if (stream.stream.mediaStreamType == MediaStreamType.SCREEN_SHARING) {
VideoType.REMOTE_SCREEN
} else {
VideoType.REMOTE_VIDEO
}
videoType =
if (stream.stream.mediaStreamType == MediaStreamType.SCREEN_SHARING) {
VideoType.REMOTE_SCREEN
} else {
VideoType.REMOTE_VIDEO
}
}
}
}
@ -90,19 +93,21 @@ internal class TestVideoStreamRendererLocalWrapper(
}
private fun createViewInternal(options: CreateViewOptions? = null): VideoStreamRendererView {
val v = synchronized(this) {
if (view != null) {
view!!
} else {
TestVideoStreamRendererView(context, videoType).also { view = it }
val v =
synchronized(this) {
if (view != null) {
view!!
} else {
TestVideoStreamRendererView(context, videoType).also { view = it }
}
}
}
return v
}
override fun dispose() = synchronized(this) {
view = null
}
override fun dispose() =
synchronized(this) {
view = null
}
override fun getStreamSize(): StreamSize? {
return view?.let {
@ -122,11 +127,12 @@ internal class TestVideoStreamRendererLocalWrapper(
}
internal class TestVideoStreamRendererView(context: Context, videoType: VideoType) : VideoStreamRendererView {
private val v = WebView(context).also {
it.settings.allowFileAccess = true
it.layoutParams = FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
it.loadUrl(fileName(videoType))
}
private val v =
WebView(context).also {
it.settings.allowFileAccess = true
it.layoutParams = FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
it.loadUrl(fileName(videoType))
}
override fun dispose() {}
@ -142,13 +148,14 @@ internal class TestVideoStreamRendererView(context: Context, videoType: VideoTyp
}
private fun fileName(videoType: VideoType): String {
val f = when (videoType) {
VideoType.REMOTE_VIDEO -> "remoteVideo.html"
VideoType.REMOTE_SCREEN -> "remoteScreenShare.html"
VideoType.LOCAL_VIDEO_BACK -> "localVideoBack.html"
VideoType.LOCAL_VIDEO_FRONT -> "localVideoFront.html"
VideoType.LOCAL_SCREEN_SHARE -> "localScreenShare.html"
}
val f =
when (videoType) {
VideoType.REMOTE_VIDEO -> "remoteVideo.html"
VideoType.REMOTE_SCREEN -> "remoteScreenShare.html"
VideoType.LOCAL_VIDEO_BACK -> "localVideoBack.html"
VideoType.LOCAL_VIDEO_FRONT -> "localVideoFront.html"
VideoType.LOCAL_SCREEN_SHARE -> "localScreenShare.html"
}
return "file:///android_asset/videoStreams/$f"
}
}

Просмотреть файл

@ -4,47 +4,47 @@ package com.azure.android.communication.ui.calling
import androidx.test.platform.app.InstrumentationRegistry
import com.azure.android.communication.BaseUiTest
import com.azure.android.communication.assertViewGone
import com.azure.android.communication.common.CommunicationTokenCredential
import com.azure.android.communication.common.CommunicationTokenRefreshOptions
import com.azure.android.communication.tapWhenDisplayed
import com.azure.android.communication.ui.calling.models.CallCompositeAudioVideoMode
import com.azure.android.communication.ui.calling.models.CallCompositeGroupCallLocator
import com.azure.android.communication.ui.calling.models.CallCompositeLocalOptions
import com.azure.android.communication.ui.calling.models.CallCompositeRemoteOptions
import com.azure.android.communication.waitUntilDisplayed
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.util.UUID
import com.azure.android.communication.assertViewGone
import com.azure.android.communication.ui.calling.models.CallCompositeAudioVideoMode
import com.azure.android.communication.ui.calling.models.CallCompositeLocalOptions
@OptIn(ExperimentalCoroutinesApi::class)
internal class AvModeTest : BaseUiTest() {
@Test
fun testAvModeDisablesCameraButtons() = runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test"
)
fun testAvModeDisablesCameraButtons() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test",
)
val localOptions = CallCompositeLocalOptions().setAudioVideoMode(CallCompositeAudioVideoMode.AUDIO_ONLY)
val localOptions = CallCompositeLocalOptions().setAudioVideoMode(CallCompositeAudioVideoMode.AUDIO_ONLY)
callComposite.launchTest(appContext, remoteOptions, localOptions)
waitUntilDisplayed(joinCallId)
assertViewGone(setupCameraButtonId)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
assertViewGone(callCameraButtonId)
}
callComposite.launchTest(appContext, remoteOptions, localOptions)
waitUntilDisplayed(joinCallId)
assertViewGone(setupCameraButtonId)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
assertViewGone(callCameraButtonId)
}
}

Просмотреть файл

@ -20,38 +20,40 @@ import java.util.UUID
internal class CallHistoryRepositoryTest {
@Test
@ExperimentalCoroutinesApi
fun callHistoryService_onCallStateUpdate_callsRepositoryInsert() = runTest {
val context: Context = ApplicationProvider.getApplicationContext()
AndroidThreeTen.init(context.applicationContext)
fun callHistoryService_onCallStateUpdate_callsRepositoryInsert() =
runTest {
val context: Context = ApplicationProvider.getApplicationContext()
AndroidThreeTen.init(context.applicationContext)
val repository: CallHistoryRepository = CallHistoryRepositoryImpl(context, DefaultLogger())
val repository: CallHistoryRepository = CallHistoryRepositoryImpl(context, DefaultLogger())
val originalList = repository.getAll()
val olderThanMonth = originalList.firstOrNull {
// should not have records older then now() - 31 days.
// Subtract a min to account possible delay while executing.
it.callStartedOn.isBefore(OffsetDateTime.now().minusDays(31).minusMinutes(1))
val originalList = repository.getAll()
val olderThanMonth =
originalList.firstOrNull {
// should not have records older then now() - 31 days.
// Subtract a min to account possible delay while executing.
it.callStartedOn.isBefore(OffsetDateTime.now().minusDays(31).minusMinutes(1))
}
Assert.assertNull(olderThanMonth)
var callId = UUID.randomUUID().toString()
var callStartDate = OffsetDateTime.now()
// inserting a record older then 31 days
repository.insert(callId, callStartDate.minusDays(32))
// should not return new record
var freshList = repository.getAll()
Assert.assertEquals(originalList.count(), freshList.count())
Assert.assertNull(freshList.find { it.callId == callId })
// inset new record
repository.insert(callId, callStartDate)
freshList = repository.getAll()
Assert.assertEquals(originalList.count() + 1, freshList.count())
val retrievedNewRecord = freshList.find { it.callId == callId }
Assert.assertNotNull(retrievedNewRecord)
Assert.assertEquals(callStartDate, retrievedNewRecord!!.callStartedOn)
}
Assert.assertNull(olderThanMonth)
var callId = UUID.randomUUID().toString()
var callStartDate = OffsetDateTime.now()
// inserting a record older then 31 days
repository.insert(callId, callStartDate.minusDays(32))
// should not return new record
var freshList = repository.getAll()
Assert.assertEquals(originalList.count(), freshList.count())
Assert.assertNull(freshList.find { it.callId == callId })
// inset new record
repository.insert(callId, callStartDate)
freshList = repository.getAll()
Assert.assertEquals(originalList.count() + 1, freshList.count())
val retrievedNewRecord = freshList.find { it.callId == callId }
Assert.assertNotNull(retrievedNewRecord)
Assert.assertEquals(callStartDate, retrievedNewRecord!!.callStartedOn)
}
}

Просмотреть файл

@ -12,92 +12,93 @@ import com.azure.android.communication.ui.calling.models.CallCompositeCallStateC
import com.azure.android.communication.ui.calling.models.CallCompositeGroupCallLocator
import com.azure.android.communication.ui.calling.models.CallCompositeRemoteOptions
import com.azure.android.communication.waitUntilDisplayed
import java.util.UUID
import java9.util.concurrent.CompletableFuture
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.util.UUID
internal class CallStateEventsTest : BaseUiTest() {
@Test
fun testCallStateConnectedEvents() = runTest {
injectDependencies(testScheduler)
fun testCallStateConnectedEvents() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test"
)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test",
)
// assert state is none
assert(callComposite.callState == CallCompositeCallStateCode.NONE)
// assert state is none
assert(callComposite.callState == CallCompositeCallStateCode.NONE)
val list = mutableListOf<CallCompositeCallStateCode>()
val list = mutableListOf<CallCompositeCallStateCode>()
callComposite.addOnCallStateChangedEventHandler {
list.add(it.code)
}
callComposite.launchTest(appContext, remoteOptions, null)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
var size = list.size
assert(size == 2)
assert(list.contains(CallCompositeCallStateCode.CONNECTED))
assert(list.contains(CallCompositeCallStateCode.NONE))
assert(callComposite.callState == CallCompositeCallStateCode.CONNECTED)
}
@Test
fun testCallStateDisconnectedEvents() = runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test"
)
// assert state is none
assert(callComposite.callState == CallCompositeCallStateCode.NONE)
val list = mutableListOf<CallCompositeCallStateCode>()
val endCallCompletableFuture = CompletableFuture<Void>()
callComposite.addOnCallStateChangedEventHandler {
list.add(it.code)
if (it.code == CallCompositeCallStateCode.DISCONNECTED) {
endCallCompletableFuture.complete(null)
callComposite.addOnCallStateChangedEventHandler {
list.add(it.code)
}
}
callComposite.launchTest(appContext, remoteOptions, null)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callComposite.launchTest(appContext, remoteOptions, null)
endCallCompletableFuture.whenComplete { _, _ ->
assert(list.size == 3)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
var size = list.size
assert(size == 2)
assert(list.contains(CallCompositeCallStateCode.CONNECTED))
assert(list.contains(CallCompositeCallStateCode.NONE))
assert(list.contains(CallCompositeCallStateCode.DISCONNECTED))
assert(callComposite.callState == CallCompositeCallStateCode.DISCONNECTED)
assert(callComposite.callState == CallCompositeCallStateCode.CONNECTED)
}
@Test
fun testCallStateDisconnectedEvents() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test",
)
// assert state is none
assert(callComposite.callState == CallCompositeCallStateCode.NONE)
val list = mutableListOf<CallCompositeCallStateCode>()
val endCallCompletableFuture = CompletableFuture<Void>()
callComposite.addOnCallStateChangedEventHandler {
list.add(it.code)
if (it.code == CallCompositeCallStateCode.DISCONNECTED) {
endCallCompletableFuture.complete(null)
}
}
callComposite.launchTest(appContext, remoteOptions, null)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
endCallCompletableFuture.whenComplete { _, _ ->
assert(list.size == 3)
assert(list.contains(CallCompositeCallStateCode.CONNECTED))
assert(list.contains(CallCompositeCallStateCode.NONE))
assert(list.contains(CallCompositeCallStateCode.DISCONNECTED))
assert(callComposite.callState == CallCompositeCallStateCode.DISCONNECTED)
}
}
}
}

Просмотреть файл

@ -12,93 +12,94 @@ import com.azure.android.communication.ui.calling.models.CallCompositeCallStateC
import com.azure.android.communication.ui.calling.models.CallCompositeGroupCallLocator
import com.azure.android.communication.ui.calling.models.CallCompositeRemoteOptions
import com.azure.android.communication.waitUntilDisplayed
import java.util.UUID
import java9.util.concurrent.CompletableFuture
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.util.UUID
internal class CompositeExitAPITest : BaseUiTest() {
@Test
fun testCompositeExitSuccessWhenStateIsConnected() = runTest {
injectDependencies(testScheduler)
fun testCompositeExitSuccessWhenStateIsConnected() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test"
)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test",
)
// assert state is none
assert(callComposite.callState == CallCompositeCallStateCode.NONE)
var isExitCompositeReceived = false
val exitCallCompletableFuture = CompletableFuture<Void>()
callComposite.addOnDismissedEventHandler {
isExitCompositeReceived = true
exitCallCompletableFuture.complete(null)
}
callComposite.launchTest(appContext, remoteOptions, null)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
assert(callComposite.callState == CallCompositeCallStateCode.CONNECTED)
// end call
callComposite.dismiss()
exitCallCompletableFuture.whenComplete { _, _ ->
assert(isExitCompositeReceived)
assert(callComposite.callState == CallCompositeCallStateCode.DISCONNECTED)
}
}
@Test
fun testCompositeExitSuccessWhenStateIsNone() = runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test"
)
// assert state is none
assert(callComposite.callState == CallCompositeCallStateCode.NONE)
var isExitCompositeReceived = false
val exitCallCompletableFuture = CompletableFuture<Void>()
callComposite.addOnDismissedEventHandler {
isExitCompositeReceived = true
exitCallCompletableFuture.complete(null)
}
callComposite.launchTest(appContext, remoteOptions, null)
waitUntilDisplayed(joinCallId)
assert(callComposite.callState == CallCompositeCallStateCode.NONE)
// end call
callComposite.dismiss()
exitCallCompletableFuture.whenComplete { _, _ ->
assert(isExitCompositeReceived)
// assert state is none
assert(callComposite.callState == CallCompositeCallStateCode.NONE)
var isExitCompositeReceived = false
val exitCallCompletableFuture = CompletableFuture<Void>()
callComposite.addOnDismissedEventHandler {
isExitCompositeReceived = true
exitCallCompletableFuture.complete(null)
}
callComposite.launchTest(appContext, remoteOptions, null)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
assert(callComposite.callState == CallCompositeCallStateCode.CONNECTED)
// end call
callComposite.dismiss()
exitCallCompletableFuture.whenComplete { _, _ ->
assert(isExitCompositeReceived)
assert(callComposite.callState == CallCompositeCallStateCode.DISCONNECTED)
}
}
@Test
fun testCompositeExitSuccessWhenStateIsNone() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test",
)
// assert state is none
assert(callComposite.callState == CallCompositeCallStateCode.NONE)
var isExitCompositeReceived = false
val exitCallCompletableFuture = CompletableFuture<Void>()
callComposite.addOnDismissedEventHandler {
isExitCompositeReceived = true
exitCallCompletableFuture.complete(null)
}
callComposite.launchTest(appContext, remoteOptions, null)
waitUntilDisplayed(joinCallId)
assert(callComposite.callState == CallCompositeCallStateCode.NONE)
// end call
callComposite.dismiss()
exitCallCompletableFuture.whenComplete { _, _ ->
assert(isExitCompositeReceived)
assert(callComposite.callState == CallCompositeCallStateCode.NONE)
}
}
}
}

Просмотреть файл

@ -1,485 +1,502 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.calling
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import com.azure.android.communication.BaseUiTest
import com.azure.android.communication.assertViewHasChild
import com.azure.android.communication.assertViewNotDisplayed
import com.azure.android.communication.assertViewText
import com.azure.android.communication.calling.MediaStreamType
import com.azure.android.communication.calling.ParticipantState
import com.azure.android.communication.common.CommunicationTokenCredential
import com.azure.android.communication.common.CommunicationTokenRefreshOptions
import com.azure.android.communication.tapOnScreen
import com.azure.android.communication.tapWhenDisplayed
import com.azure.android.communication.tapWithTextWhenDisplayed
import com.azure.android.communication.ui.calling.models.CallCompositeGroupCallLocator
import com.azure.android.communication.ui.calling.models.CallCompositeInternalParticipantRole
import com.azure.android.communication.ui.calling.models.CallCompositeRemoteOptions
import com.azure.android.communication.ui.calling.models.CallCompositeTeamsMeetingLinkLocator
import com.azure.android.communication.ui.calling.service.sdk.CommunicationIdentifier
import com.azure.android.communication.waitUntilDisplayed
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.util.UUID
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.ui.calling.models.CallCompositeLobbyErrorCode
import java9.util.concurrent.CompletableFuture
internal class LobbyTest : BaseUiTest() {
@Test
fun testOnGridViewLobbyParticipantAreNotVisible() = runTest {
injectDependencies(testScheduler)
val expectedParticipantCountOnGridView = 2
val expectedParticipantCountOnParticipantList = 3
val expectedParticipantCountOnFloatingHeader = 2
lobbyParticipantsVisibilityTests(
expectedParticipantCountOnFloatingHeader,
expectedParticipantCountOnGridView,
expectedParticipantCountOnParticipantList
)
}
@Test
fun testOnGridViewAndParticipantListAllConnectedParticipantsAreVisible() = runTest {
injectDependencies(testScheduler)
val expectedParticipantCountOnGridView = 2
val expectedParticipantCountOnParticipantList = 3
val expectedParticipantCountOnFloatingHeader = 2
lobbyParticipantsVisibilityTests(
expectedParticipantCountOnFloatingHeader,
expectedParticipantCountOnGridView,
expectedParticipantCountOnParticipantList,
false
)
}
@Test
fun testOnLobbyParticipantNotDisplayedIfParticipantRoleIsAttendee() = runTest {
injectDependencies(testScheduler)
// Launch the UI.
joinTeamsCall()
callingSDK.setParticipantRoleSharedFlow(CallCompositeInternalParticipantRole.ATTENDEE)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY
)
// assert lobby header is not displayed
assertViewNotDisplayed(lobbyHeaderId)
}
@Test
fun testOnLobbyParticipantAddLobbyHeaderIsDisplayed() = runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = joinTeamsCall()
callingSDK.setParticipantRoleSharedFlow(CallCompositeInternalParticipantRole.PRESENTER)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY
)
waitUntilDisplayed(lobbyHeaderId)
// assert lobby header is displayed
assertViewText(lobbyHeaderText, appContext!!.getString(R.string.azure_communication_ui_calling_lobby_header_text))
callingSDK.removeParticipant("ACS User 2")
// assert lobby header is not displayed
assertViewNotDisplayed(lobbyHeaderId)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 3"),
displayName = "ACS User 3",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY
)
waitUntilDisplayed(lobbyHeaderId)
tapWhenDisplayed(lobbyHeaderOpenParticipantListButton)
// one local + one remote + 2 texts (calling, lobby)
assertViewHasChild(bottomDrawer, 4)
}
@Test
fun testOnLobbyHeaderCloseButtonPressLobbyHeaderIsClosed() = runTest {
injectDependencies(testScheduler)
// launch the UI.
joinTeamsCall()
callingSDK.setParticipantRoleSharedFlow(CallCompositeInternalParticipantRole.PRESENTER)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY
)
waitUntilDisplayed(lobbyHeaderId)
tapWhenDisplayed(lobbyHeaderCloseButton)
assertViewNotDisplayed(lobbyHeaderId)
}
@Test
fun testOnAdmitErrorLobbyErrorHeaderIsDisplayed() = runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Admit"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(buttonTextToClick, appContext)
}
@Test
fun testOnDeclineErrorLobbyErrorHeaderIsDisplayed() = runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Decline"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(buttonTextToClick, appContext)
}
@Test
fun testOnAdmitAllErrorLobbyErrorHeaderIsDisplayed() = runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = joinTeamsCall()
callingSDK.setParticipantRoleSharedFlow(CallCompositeInternalParticipantRole.PRESENTER)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY
)
waitUntilDisplayed(lobbyHeaderId)
tapWhenDisplayed(lobbyHeaderOpenParticipantListButton)
// one local + one remote + 2 texts (calling, lobby)
assertViewHasChild(bottomDrawer, 4)
val lobbyActionResult = CompletableFuture<CallCompositeLobbyErrorCode?>()
lobbyActionResult.complete(CallCompositeLobbyErrorCode.LOBBY_MEETING_ROLE_NOT_ALLOWED)
callingSDK.setLobbyResultCompletableFuture(lobbyActionResult)
// click on admit all
tapWithTextWhenDisplayed("Admit all")
// to close participant list
tapOnScreen()
waitUntilDisplayed(lobbyErrorHeaderId)
// assert error text
assertViewText(
lobbyErrorHeaderText,
appContext!!.getString(R.string.azure_communication_ui_calling_error_lobby_meeting_role_not_allowded)
)
// close lobby error header
tapWhenDisplayed(lobbyErrorHeaderCloseButton)
// assert lobby error header is not displayed
assertViewNotDisplayed(lobbyErrorHeaderId)
}
@Test
fun testOnAdmitAllSuccess() = runTest {
injectDependencies(testScheduler)
// Launch the UI.
joinTeamsCall()
callingSDK.setParticipantRoleSharedFlow(CallCompositeInternalParticipantRole.PRESENTER)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY
)
waitUntilDisplayed(lobbyHeaderId)
tapWhenDisplayed(lobbyHeaderOpenParticipantListButton)
// one local + one remote + 2 texts (calling, lobby)
assertViewHasChild(bottomDrawer, 4)
val lobbyActionResult = CompletableFuture<CallCompositeLobbyErrorCode?>()
callingSDK.setLobbyResultCompletableFuture(lobbyActionResult)
// click on admit all
tapWithTextWhenDisplayed("Admit all")
// to close participant list
tapOnScreen()
callingSDK.changeParticipantState("ACS User 2", ParticipantState.CONNECTED)
lobbyActionResult.complete(null)
// assert lobby error header is not displayed
assertViewNotDisplayed(lobbyErrorHeaderId)
// tap on screen
tapOnScreen()
tapWhenDisplayed(participantListOpenButton)
// one local + one remote + 1 texts (calling)
assertViewHasChild(bottomDrawer, 3)
}
@Test
fun testOnLobbyErrorMessageUIForRoleNotPermitted() = runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Admit"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(
buttonTextToClick, appContext,
CallCompositeLobbyErrorCode.LOBBY_MEETING_ROLE_NOT_ALLOWED,
R.string.azure_communication_ui_calling_error_lobby_meeting_role_not_allowded
)
}
@Test
fun testOnLobbyErrorMessageUIForMeetingTypeNotSupported() = runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Admit"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(
buttonTextToClick, appContext,
CallCompositeLobbyErrorCode.LOBBY_CONVERSATION_TYPE_NOT_SUPPORTED,
R.string.azure_communication_ui_calling_error_lobby_conversation_type_not_supported
)
}
@Test
fun testOnLobbyErrorMessageUIForFailedToRemove() = runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Admit"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(
buttonTextToClick, appContext,
CallCompositeLobbyErrorCode.REMOVE_PARTICIPANT_OPERATION_FAILURE,
R.string.azure_communication_ui_calling_error_lobby_failed_to_remove_participant
)
}
@Test
fun testOnLobbyErrorMessageUIForUnknownError() = runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Admit"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(
buttonTextToClick, appContext,
CallCompositeLobbyErrorCode.UNKNOWN_ERROR,
R.string.azure_communication_ui_calling_error_lobby_unknown
)
}
@Test
fun testOnLobbyErrorMessageUIForLobbyDisabledError() = runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Admit"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(
buttonTextToClick, appContext,
CallCompositeLobbyErrorCode.LOBBY_DISABLED_BY_CONFIGURATIONS,
R.string.azure_communication_ui_calling_error_lobby_disabled_by_configuration
)
}
private suspend fun lobbyButtonActionsTest(
buttonTextToClick: String,
appContext: Context?,
errorCode: CallCompositeLobbyErrorCode = CallCompositeLobbyErrorCode.LOBBY_MEETING_ROLE_NOT_ALLOWED,
errorUIId: Int = R.string.azure_communication_ui_calling_error_lobby_meeting_role_not_allowded
) {
callingSDK.setParticipantRoleSharedFlow(CallCompositeInternalParticipantRole.PRESENTER)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY
)
waitUntilDisplayed(lobbyHeaderId)
tapWhenDisplayed(lobbyHeaderOpenParticipantListButton)
// one local + one remote + 2 texts (calling, lobby)
assertViewHasChild(bottomDrawer, 4)
val lobbyActionResult = CompletableFuture<CallCompositeLobbyErrorCode?>()
lobbyActionResult.complete(errorCode)
callingSDK.setLobbyResultCompletableFuture(lobbyActionResult)
tapWithTextWhenDisplayed("ACS User 2")
tapWithTextWhenDisplayed(buttonTextToClick)
// to close participant list
tapOnScreen()
waitUntilDisplayed(lobbyErrorHeaderId)
// assert error text
assertViewText(
lobbyErrorHeaderText,
appContext!!.getString(errorUIId)
)
// close lobby error header
tapWhenDisplayed(lobbyErrorHeaderCloseButton)
// assert lobby error header is not displayed
assertViewNotDisplayed(lobbyErrorHeaderId)
}
private fun joinTeamsCall(): Context? {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeTeamsMeetingLinkLocator("https:teams.meeting"),
communicationTokenCredential,
"test"
)
callComposite.launchTest(appContext, remoteOptions, null)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
return appContext
}
private suspend fun lobbyParticipantsVisibilityTests(
expectedParticipantCountOnFloatingHeader: Int,
expectedParticipantCountOnGridView: Int,
expectedParticipantCountOnParticipantList: Int,
addLobbyUser: Boolean = true
) {
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test"
)
callComposite.launchTest(appContext, remoteOptions, null)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 1"),
displayName = "ACS User 1",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO)
)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO)
)
if (addLobbyUser) {
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("Lobby State"),
displayName = "Lobby State",
state = ParticipantState.IN_LOBBY,
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO)
)
}
waitUntilDisplayed(participantContainerId)
assertViewText(
participantCountId,
"Call with $expectedParticipantCountOnFloatingHeader people"
)
assertViewHasChild(participantContainerId, expectedParticipantCountOnGridView)
tapWhenDisplayed(participantListOpenButton)
// 1 local
assertViewHasChild(bottomDrawer, expectedParticipantCountOnParticipantList + 1)
}
}
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.calling
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import com.azure.android.communication.BaseUiTest
import com.azure.android.communication.assertViewHasChild
import com.azure.android.communication.assertViewNotDisplayed
import com.azure.android.communication.assertViewText
import com.azure.android.communication.calling.MediaStreamType
import com.azure.android.communication.calling.ParticipantState
import com.azure.android.communication.common.CommunicationTokenCredential
import com.azure.android.communication.common.CommunicationTokenRefreshOptions
import com.azure.android.communication.tapOnScreen
import com.azure.android.communication.tapWhenDisplayed
import com.azure.android.communication.tapWithTextWhenDisplayed
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.ui.calling.models.CallCompositeGroupCallLocator
import com.azure.android.communication.ui.calling.models.CallCompositeInternalParticipantRole
import com.azure.android.communication.ui.calling.models.CallCompositeLobbyErrorCode
import com.azure.android.communication.ui.calling.models.CallCompositeRemoteOptions
import com.azure.android.communication.ui.calling.models.CallCompositeTeamsMeetingLinkLocator
import com.azure.android.communication.ui.calling.service.sdk.CommunicationIdentifier
import com.azure.android.communication.waitUntilDisplayed
import java9.util.concurrent.CompletableFuture
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.util.UUID
internal class LobbyTest : BaseUiTest() {
@Test
fun testOnGridViewLobbyParticipantAreNotVisible() =
runTest {
injectDependencies(testScheduler)
val expectedParticipantCountOnGridView = 2
val expectedParticipantCountOnParticipantList = 3
val expectedParticipantCountOnFloatingHeader = 2
lobbyParticipantsVisibilityTests(
expectedParticipantCountOnFloatingHeader,
expectedParticipantCountOnGridView,
expectedParticipantCountOnParticipantList,
)
}
@Test
fun testOnGridViewAndParticipantListAllConnectedParticipantsAreVisible() =
runTest {
injectDependencies(testScheduler)
val expectedParticipantCountOnGridView = 2
val expectedParticipantCountOnParticipantList = 3
val expectedParticipantCountOnFloatingHeader = 2
lobbyParticipantsVisibilityTests(
expectedParticipantCountOnFloatingHeader,
expectedParticipantCountOnGridView,
expectedParticipantCountOnParticipantList,
false,
)
}
@Test
fun testOnLobbyParticipantNotDisplayedIfParticipantRoleIsAttendee() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
joinTeamsCall()
callingSDK.setParticipantRoleSharedFlow(CallCompositeInternalParticipantRole.ATTENDEE)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY,
)
// assert lobby header is not displayed
assertViewNotDisplayed(lobbyHeaderId)
}
@Test
fun testOnLobbyParticipantAddLobbyHeaderIsDisplayed() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = joinTeamsCall()
callingSDK.setParticipantRoleSharedFlow(CallCompositeInternalParticipantRole.PRESENTER)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY,
)
waitUntilDisplayed(lobbyHeaderId)
// assert lobby header is displayed
assertViewText(lobbyHeaderText, appContext!!.getString(R.string.azure_communication_ui_calling_lobby_header_text))
callingSDK.removeParticipant("ACS User 2")
// assert lobby header is not displayed
assertViewNotDisplayed(lobbyHeaderId)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 3"),
displayName = "ACS User 3",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY,
)
waitUntilDisplayed(lobbyHeaderId)
tapWhenDisplayed(lobbyHeaderOpenParticipantListButton)
// one local + one remote + 2 texts (calling, lobby)
assertViewHasChild(bottomDrawer, 4)
}
@Test
fun testOnLobbyHeaderCloseButtonPressLobbyHeaderIsClosed() =
runTest {
injectDependencies(testScheduler)
// launch the UI.
joinTeamsCall()
callingSDK.setParticipantRoleSharedFlow(CallCompositeInternalParticipantRole.PRESENTER)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY,
)
waitUntilDisplayed(lobbyHeaderId)
tapWhenDisplayed(lobbyHeaderCloseButton)
assertViewNotDisplayed(lobbyHeaderId)
}
@Test
fun testOnAdmitErrorLobbyErrorHeaderIsDisplayed() =
runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Admit"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(buttonTextToClick, appContext)
}
@Test
fun testOnDeclineErrorLobbyErrorHeaderIsDisplayed() =
runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Decline"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(buttonTextToClick, appContext)
}
@Test
fun testOnAdmitAllErrorLobbyErrorHeaderIsDisplayed() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = joinTeamsCall()
callingSDK.setParticipantRoleSharedFlow(CallCompositeInternalParticipantRole.PRESENTER)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY,
)
waitUntilDisplayed(lobbyHeaderId)
tapWhenDisplayed(lobbyHeaderOpenParticipantListButton)
// one local + one remote + 2 texts (calling, lobby)
assertViewHasChild(bottomDrawer, 4)
val lobbyActionResult = CompletableFuture<CallCompositeLobbyErrorCode?>()
lobbyActionResult.complete(CallCompositeLobbyErrorCode.LOBBY_MEETING_ROLE_NOT_ALLOWED)
callingSDK.setLobbyResultCompletableFuture(lobbyActionResult)
// click on admit all
tapWithTextWhenDisplayed("Admit all")
// to close participant list
tapOnScreen()
waitUntilDisplayed(lobbyErrorHeaderId)
// assert error text
assertViewText(
lobbyErrorHeaderText,
appContext!!.getString(R.string.azure_communication_ui_calling_error_lobby_meeting_role_not_allowded),
)
// close lobby error header
tapWhenDisplayed(lobbyErrorHeaderCloseButton)
// assert lobby error header is not displayed
assertViewNotDisplayed(lobbyErrorHeaderId)
}
@Test
fun testOnAdmitAllSuccess() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
joinTeamsCall()
callingSDK.setParticipantRoleSharedFlow(CallCompositeInternalParticipantRole.PRESENTER)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY,
)
waitUntilDisplayed(lobbyHeaderId)
tapWhenDisplayed(lobbyHeaderOpenParticipantListButton)
// one local + one remote + 2 texts (calling, lobby)
assertViewHasChild(bottomDrawer, 4)
val lobbyActionResult = CompletableFuture<CallCompositeLobbyErrorCode?>()
callingSDK.setLobbyResultCompletableFuture(lobbyActionResult)
// click on admit all
tapWithTextWhenDisplayed("Admit all")
// to close participant list
tapOnScreen()
callingSDK.changeParticipantState("ACS User 2", ParticipantState.CONNECTED)
lobbyActionResult.complete(null)
// assert lobby error header is not displayed
assertViewNotDisplayed(lobbyErrorHeaderId)
// tap on screen
tapOnScreen()
tapWhenDisplayed(participantListOpenButton)
// one local + one remote + 1 texts (calling)
assertViewHasChild(bottomDrawer, 3)
}
@Test
fun testOnLobbyErrorMessageUIForRoleNotPermitted() =
runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Admit"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(
buttonTextToClick,
appContext,
CallCompositeLobbyErrorCode.LOBBY_MEETING_ROLE_NOT_ALLOWED,
R.string.azure_communication_ui_calling_error_lobby_meeting_role_not_allowded,
)
}
@Test
fun testOnLobbyErrorMessageUIForMeetingTypeNotSupported() =
runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Admit"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(
buttonTextToClick,
appContext,
CallCompositeLobbyErrorCode.LOBBY_CONVERSATION_TYPE_NOT_SUPPORTED,
R.string.azure_communication_ui_calling_error_lobby_conversation_type_not_supported,
)
}
@Test
fun testOnLobbyErrorMessageUIForFailedToRemove() =
runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Admit"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(
buttonTextToClick,
appContext,
CallCompositeLobbyErrorCode.REMOVE_PARTICIPANT_OPERATION_FAILURE,
R.string.azure_communication_ui_calling_error_lobby_failed_to_remove_participant,
)
}
@Test
fun testOnLobbyErrorMessageUIForUnknownError() =
runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Admit"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(
buttonTextToClick,
appContext,
CallCompositeLobbyErrorCode.UNKNOWN_ERROR,
R.string.azure_communication_ui_calling_error_lobby_unknown,
)
}
@Test
fun testOnLobbyErrorMessageUIForLobbyDisabledError() =
runTest {
injectDependencies(testScheduler)
val buttonTextToClick = "Admit"
// Launch the UI.
val appContext = joinTeamsCall()
lobbyButtonActionsTest(
buttonTextToClick,
appContext,
CallCompositeLobbyErrorCode.LOBBY_DISABLED_BY_CONFIGURATIONS,
R.string.azure_communication_ui_calling_error_lobby_disabled_by_configuration,
)
}
private suspend fun lobbyButtonActionsTest(
buttonTextToClick: String,
appContext: Context?,
errorCode: CallCompositeLobbyErrorCode = CallCompositeLobbyErrorCode.LOBBY_MEETING_ROLE_NOT_ALLOWED,
errorUIId: Int = R.string.azure_communication_ui_calling_error_lobby_meeting_role_not_allowded,
) {
callingSDK.setParticipantRoleSharedFlow(CallCompositeInternalParticipantRole.PRESENTER)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
state = ParticipantState.IN_LOBBY,
)
waitUntilDisplayed(lobbyHeaderId)
tapWhenDisplayed(lobbyHeaderOpenParticipantListButton)
// one local + one remote + 2 texts (calling, lobby)
assertViewHasChild(bottomDrawer, 4)
val lobbyActionResult = CompletableFuture<CallCompositeLobbyErrorCode?>()
lobbyActionResult.complete(errorCode)
callingSDK.setLobbyResultCompletableFuture(lobbyActionResult)
tapWithTextWhenDisplayed("ACS User 2")
tapWithTextWhenDisplayed(buttonTextToClick)
// to close participant list
tapOnScreen()
waitUntilDisplayed(lobbyErrorHeaderId)
// assert error text
assertViewText(
lobbyErrorHeaderText,
appContext!!.getString(errorUIId),
)
// close lobby error header
tapWhenDisplayed(lobbyErrorHeaderCloseButton)
// assert lobby error header is not displayed
assertViewNotDisplayed(lobbyErrorHeaderId)
}
private fun joinTeamsCall(): Context? {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeTeamsMeetingLinkLocator("https:teams.meeting"),
communicationTokenCredential,
"test",
)
callComposite.launchTest(appContext, remoteOptions, null)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
return appContext
}
private suspend fun lobbyParticipantsVisibilityTests(
expectedParticipantCountOnFloatingHeader: Int,
expectedParticipantCountOnGridView: Int,
expectedParticipantCountOnParticipantList: Int,
addLobbyUser: Boolean = true,
) {
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test",
)
callComposite.launchTest(appContext, remoteOptions, null)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 1"),
displayName = "ACS User 1",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
)
if (addLobbyUser) {
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("Lobby State"),
displayName = "Lobby State",
state = ParticipantState.IN_LOBBY,
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.VIDEO),
)
}
waitUntilDisplayed(participantContainerId)
assertViewText(
participantCountId,
"Call with $expectedParticipantCountOnFloatingHeader people",
)
assertViewHasChild(participantContainerId, expectedParticipantCountOnGridView)
tapWhenDisplayed(participantListOpenButton)
// 1 local
assertViewHasChild(bottomDrawer, expectedParticipantCountOnParticipantList + 1)
}
}

Просмотреть файл

@ -5,117 +5,120 @@ package com.azure.android.communication.ui.calling
import com.azure.android.communication.BaseUiTest
import com.azure.android.communication.assertViewText
import org.junit.Test
import com.azure.android.communication.calling.MediaStreamType
import com.azure.android.communication.tapWhenDisplayed
import com.azure.android.communication.ui.calling.service.sdk.CommunicationIdentifier
import com.azure.android.communication.waitUntilDisplayed
import kotlinx.coroutines.test.runTest
import org.junit.Test
internal class RemoteParticipantCounterTest : BaseUiTest() {
@Test
fun testInitiallyPopulatedCallDisplaysCorrectParticipantCounter() = runTest {
injectDependencies(testScheduler)
fun testInitiallyPopulatedCallDisplaysCorrectParticipantCounter() =
runTest {
injectDependencies(testScheduler)
// Add participants that are already present on the call before we join it.
callingSDK.addRemoteParticipant(
CommunicationIdentifier.MicrosoftTeamsUserIdentifier("Teams User 1", true),
displayName = "Teams User 1"
)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.MicrosoftTeamsUserIdentifier("Teams User 2", false),
displayName = "Teams User 2",
videoStreams = listOf(MediaStreamType.VIDEO)
)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.PhoneNumberIdentifier("16047891234"),
displayName = "16047891234"
)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 1"),
displayName = "ACS User 1",
videoStreams = listOf(MediaStreamType.SCREEN_SHARING)
)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Verify we're displaying correct participant count.
assertViewText(participantCountId, "Call with 4 people")
// One of the remote participants drops from the call.
callingSDK.removeParticipant("ACS User 1")
// Verify we're displaying a reduced participant count.
assertViewText(participantCountId, "Call with 3 people")
}
@Test
fun testInitiallyEmptyCallDisplaysCorrectParticipantCounter() = runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Initial label with no remote participants present.
assertViewText(participantCountId, "Waiting for others to join")
// Add participants that are already present on the call before we join it.
callingSDK.addRemoteParticipant(
CommunicationIdentifier.MicrosoftTeamsUserIdentifier("Teams User 1", true),
displayName = "Teams User 1"
)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.MicrosoftTeamsUserIdentifier("Teams User 2", false),
displayName = "Teams User 2",
videoStreams = listOf(MediaStreamType.VIDEO)
)
// Verify we're displaying correct participant count.
assertViewText(participantCountId, "Call with 2 people")
// One of the remote participants drops from the call.
callingSDK.removeParticipant("Teams User 1")
// Verify we're displaying a reduced participant count.
assertViewText(participantCountId, "Call with 1 person")
// Back to where we started, no participants.
callingSDK.removeParticipant("Teams User 2")
assertViewText(participantCountId, "Waiting for others to join")
}
@Test
fun testLargeNumberOfParticipants() = runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
val maxNumberOfPeople = 200
// Add a bunch of remote participants into the call.
repeat(maxNumberOfPeople) { id ->
// Add participants that are already present on the call before we join it.
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User $id"),
displayName = "ACS User $id",
videoStreams = listOf(MediaStreamType.VIDEO)
CommunicationIdentifier.MicrosoftTeamsUserIdentifier("Teams User 1", true),
displayName = "Teams User 1",
)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.MicrosoftTeamsUserIdentifier("Teams User 2", false),
displayName = "Teams User 2",
videoStreams = listOf(MediaStreamType.VIDEO),
)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.PhoneNumberIdentifier("16047891234"),
displayName = "16047891234",
)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 1"),
displayName = "ACS User 1",
videoStreams = listOf(MediaStreamType.SCREEN_SHARING),
)
if (id == 0) {
assertViewText(participantCountId, "Call with 1 person")
} else {
assertViewText(participantCountId, "Call with ${id + 1} people")
}
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Verify we're displaying correct participant count.
assertViewText(participantCountId, "Call with 4 people")
// One of the remote participants drops from the call.
callingSDK.removeParticipant("ACS User 1")
// Verify we're displaying a reduced participant count.
assertViewText(participantCountId, "Call with 3 people")
}
assertViewText(participantCountId, "Call with $maxNumberOfPeople people")
}
@Test
fun testInitiallyEmptyCallDisplaysCorrectParticipantCounter() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Initial label with no remote participants present.
assertViewText(participantCountId, "Waiting for others to join")
// Add participants that are already present on the call before we join it.
callingSDK.addRemoteParticipant(
CommunicationIdentifier.MicrosoftTeamsUserIdentifier("Teams User 1", true),
displayName = "Teams User 1",
)
callingSDK.addRemoteParticipant(
CommunicationIdentifier.MicrosoftTeamsUserIdentifier("Teams User 2", false),
displayName = "Teams User 2",
videoStreams = listOf(MediaStreamType.VIDEO),
)
// Verify we're displaying correct participant count.
assertViewText(participantCountId, "Call with 2 people")
// One of the remote participants drops from the call.
callingSDK.removeParticipant("Teams User 1")
// Verify we're displaying a reduced participant count.
assertViewText(participantCountId, "Call with 1 person")
// Back to where we started, no participants.
callingSDK.removeParticipant("Teams User 2")
assertViewText(participantCountId, "Waiting for others to join")
}
@Test
fun testLargeNumberOfParticipants() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
val maxNumberOfPeople = 200
// Add a bunch of remote participants into the call.
repeat(maxNumberOfPeople) { id ->
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User $id"),
displayName = "ACS User $id",
videoStreams = listOf(MediaStreamType.VIDEO),
)
if (id == 0) {
assertViewText(participantCountId, "Call with 1 person")
} else {
assertViewText(participantCountId, "Call with ${id + 1} people")
}
}
assertViewText(participantCountId, "Call with $maxNumberOfPeople people")
}
}

Просмотреть файл

@ -14,54 +14,55 @@ import org.junit.Test
internal class RemoteParticipantEventsTest : BaseUiTest() {
@Test
fun testRemoteParticipantBasicEvents() = runTest {
injectDependencies(testScheduler)
fun testRemoteParticipantBasicEvents() =
runTest {
injectDependencies(testScheduler)
// A demonstration of how to invoke remote participant events.
// A demonstration of how to invoke remote participant events.
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
"ACS User 1".also { userId ->
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier(userId),
displayName = userId,
isMuted = true,
isSpeaking = false,
videoStreams = listOf(MediaStreamType.VIDEO),
)
// todo assertions
// verify muted icon is displayed
callingSDK.changeParticipant(
userId,
isMuted = false,
isSpeaking = true,
)
// verify muted icon is absent
// verify isSpeaking frame
callingSDK.changeParticipant(
userId,
state = ParticipantState.DISCONNECTED,
)
// verify participant state change
}
"ACS User 1".also { userId ->
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier(userId),
displayName = userId,
isMuted = true,
isSpeaking = false,
videoStreams = listOf(MediaStreamType.VIDEO)
)
// todo assertions
// verify muted icon is displayed
callingSDK.changeParticipant(
userId,
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.SCREEN_SHARING),
)
// verify muted icon is absent
// verify main speaker switched
// verify isSpeaking frame
callingSDK.changeParticipant(
userId,
state = ParticipantState.DISCONNECTED
)
// verify participant state change
}
callingSDK.addRemoteParticipant(
CommunicationIdentifier.CommunicationUserIdentifier("ACS User 2"),
displayName = "ACS User 2",
isMuted = false,
isSpeaking = true,
videoStreams = listOf(MediaStreamType.SCREEN_SHARING)
)
// verify main speaker switched
// verify isSpeaking frame
}
}

Просмотреть файл

@ -1,94 +1,95 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.calling
import androidx.test.platform.app.InstrumentationRegistry
import com.azure.android.communication.BaseUiTest
import com.azure.android.communication.common.CommunicationTokenCredential
import com.azure.android.communication.common.CommunicationTokenRefreshOptions
import com.azure.android.communication.tapOnText
import com.azure.android.communication.tapWhenDisplayed
import com.azure.android.communication.ui.calling.models.CallCompositeGroupCallLocator
import com.azure.android.communication.ui.calling.models.CallCompositeRemoteOptions
import com.azure.android.communication.ui.calling.models.CallCompositeUserReportedIssueEvent
import com.azure.android.communication.waitUntilDisplayed
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.util.UUID
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers.withId
import com.azure.android.communication.assertTextNotDisplayed
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@OptIn(ExperimentalCoroutinesApi::class)
internal class SupportFormTest : BaseUiTest() {
@Test
fun testSupportFormIsDisplayedAndSendsEvent() = runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test"
)
var event: CallCompositeUserReportedIssueEvent? = null
callComposite.addOnUserReportedEventHandler {
event = it
}
callComposite.launchTest(appContext, remoteOptions, null)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
tapWhenDisplayed(moreOptionsId)
tapOnText(showSupportFormTextId)
waitUntilDisplayed(userMessageEditTextId)
val testMessage = "Test support message"
onView(withId(userMessageEditTextId))
.perform(ViewActions.typeText(testMessage))
tapWhenDisplayed(sendButtonId)
assertNotNull(event)
assertEquals(testMessage, event?.userMessage)
}
@Test
fun testSupportFormIsNotDisplayedWhenNoHandler() = runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test"
)
callComposite.launchTest(appContext, remoteOptions, null)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
tapWhenDisplayed(moreOptionsId)
assertTextNotDisplayed(showSupportFormTextId)
}
}
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.calling
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.platform.app.InstrumentationRegistry
import com.azure.android.communication.BaseUiTest
import com.azure.android.communication.assertTextNotDisplayed
import com.azure.android.communication.common.CommunicationTokenCredential
import com.azure.android.communication.common.CommunicationTokenRefreshOptions
import com.azure.android.communication.tapOnText
import com.azure.android.communication.tapWhenDisplayed
import com.azure.android.communication.ui.calling.models.CallCompositeGroupCallLocator
import com.azure.android.communication.ui.calling.models.CallCompositeRemoteOptions
import com.azure.android.communication.ui.calling.models.CallCompositeUserReportedIssueEvent
import com.azure.android.communication.waitUntilDisplayed
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import java.util.UUID
@OptIn(ExperimentalCoroutinesApi::class)
internal class SupportFormTest : BaseUiTest() {
@Test
fun testSupportFormIsDisplayedAndSendsEvent() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test",
)
var event: CallCompositeUserReportedIssueEvent? = null
callComposite.addOnUserReportedEventHandler {
event = it
}
callComposite.launchTest(appContext, remoteOptions, null)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
tapWhenDisplayed(moreOptionsId)
tapOnText(showSupportFormTextId)
waitUntilDisplayed(userMessageEditTextId)
val testMessage = "Test support message"
onView(withId(userMessageEditTextId))
.perform(ViewActions.typeText(testMessage))
tapWhenDisplayed(sendButtonId)
assertNotNull(event)
assertEquals(testMessage, event?.userMessage)
}
@Test
fun testSupportFormIsNotDisplayedWhenNoHandler() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val callComposite = CallCompositeBuilder().build()
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test",
)
callComposite.launchTest(appContext, remoteOptions, null)
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
tapWhenDisplayed(moreOptionsId)
assertTextNotDisplayed(showSupportFormTextId)
}
}

Просмотреть файл

@ -7,224 +7,232 @@ import com.azure.android.communication.BaseUiTest
import com.azure.android.communication.assertDisplayed
import com.azure.android.communication.assertViewGone
import com.azure.android.communication.assertViewText
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.tapWhenDisplayed
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.waitUntilDisplayed
import kotlinx.coroutines.test.runTest
import org.junit.Test
internal class ToastNotificationTest : BaseUiTest() {
@Test
fun testShowLowNetworkReceiveQualityToastNotification() = runTest {
injectDependencies(testScheduler)
fun testShowLowNetworkReceiveQualityToastNotification() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.setLowNetworkRecieveQuality(true)
callingSDK.setLowNetworkRecieveQuality(true)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_quality_low)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_quality_low)
callingSDK.setLowNetworkRecieveQuality(false)
callingSDK.setLowNetworkRecieveQuality(false)
assertViewGone(toastNotificationId)
assertViewGone(toastNotificationIconId)
}
assertViewGone(toastNotificationId)
assertViewGone(toastNotificationIconId)
}
@Test
fun testShowLowNetworkSendQualityToastNotification() = runTest {
injectDependencies(testScheduler)
fun testShowLowNetworkSendQualityToastNotification() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.setLowNetworkSendQuality(true)
callingSDK.setLowNetworkSendQuality(true)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_quality_low)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_quality_low)
callingSDK.setLowNetworkSendQuality(false)
callingSDK.setLowNetworkSendQuality(false)
assertViewGone(toastNotificationId)
assertViewGone(toastNotificationIconId)
}
assertViewGone(toastNotificationId)
assertViewGone(toastNotificationIconId)
}
@Test
fun testShowLowNetworkReconnectionQualityToastNotification() = runTest {
injectDependencies(testScheduler)
fun testShowLowNetworkReconnectionQualityToastNotification() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.setLowNetworkReconnectionQuality(true)
callingSDK.setLowNetworkReconnectionQuality(true)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_reconnecting)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_reconnecting)
// Stop speaking while muted
callingSDK.setLowNetworkReconnectionQuality(false)
// Stop speaking while muted
callingSDK.setLowNetworkReconnectionQuality(false)
assertViewGone(toastNotificationId)
assertViewGone(toastNotificationIconId)
}
assertViewGone(toastNotificationId)
assertViewGone(toastNotificationIconId)
}
@Test
fun testShowNetworkUnavailableToastNotification() = runTest {
injectDependencies(testScheduler)
fun testShowNetworkUnavailableToastNotification() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.setNetworkUnavailable(true)
callingSDK.setNetworkUnavailable(true)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_was_lost)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_was_lost)
// Stop speaking while muted
callingSDK.setNetworkUnavailable(false)
// Stop speaking while muted
callingSDK.setNetworkUnavailable(false)
// Assert toast notification is still shown even after UFD is set to false
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_was_lost)
}
// Assert toast notification is still shown even after UFD is set to false
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_was_lost)
}
@Test
fun testShowNetworkRelaysUnreachableToastNotification() = runTest {
injectDependencies(testScheduler)
fun testShowNetworkRelaysUnreachableToastNotification() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.setNetworkRelaysUnreachable(true)
callingSDK.setNetworkRelaysUnreachable(true)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_was_lost)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_was_lost)
// Stop speaking while muted
callingSDK.setNetworkRelaysUnreachable(false)
// Stop speaking while muted
callingSDK.setNetworkRelaysUnreachable(false)
// Assert toast notification is still shown even after UFD is set to false
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_was_lost)
}
// Assert toast notification is still shown even after UFD is set to false
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_network_was_lost)
}
@Test
fun testShowSpeakingWhileMutedToastNotification() = runTest {
injectDependencies(testScheduler)
fun testShowSpeakingWhileMutedToastNotification() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.setSpeakingWhileMuted(true)
callingSDK.setSpeakingWhileMuted(true)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_you_are_muted)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_you_are_muted)
// Stop speaking while muted
callingSDK.setSpeakingWhileMuted(false)
// Stop speaking while muted
callingSDK.setSpeakingWhileMuted(false)
assertViewGone(toastNotificationId)
assertViewGone(toastNotificationIconId)
}
assertViewGone(toastNotificationId)
assertViewGone(toastNotificationIconId)
}
@Test
fun testShowCameraStartFailedToastNotification() = runTest {
injectDependencies(testScheduler)
fun testShowCameraStartFailedToastNotification() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.setCameraStartFailed(true)
callingSDK.setCameraStartFailed(true)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_unable_to_start_camera)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_unable_to_start_camera)
// Stop speaking while muted
callingSDK.setCameraStartFailed(false)
// Stop speaking while muted
callingSDK.setCameraStartFailed(false)
// Assert toast notification is still shown even after UFD is set to false
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_unable_to_start_camera)
}
// Assert toast notification is still shown even after UFD is set to false
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_unable_to_start_camera)
}
@Test
fun testShowCameraStartTimedOutToastNotification() = runTest {
injectDependencies(testScheduler)
fun testShowCameraStartTimedOutToastNotification() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.setCameraStartTimedOut(true)
callingSDK.setCameraStartTimedOut(true)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Check that the toast notification appeared
waitUntilDisplayed(toastNotificationId)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_unable_to_start_camera)
// Assert toast notification appears with correct text
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_unable_to_start_camera)
// Stop speaking while muted
callingSDK.setCameraStartTimedOut(false)
// Stop speaking while muted
callingSDK.setCameraStartTimedOut(false)
// Assert toast notification is still shown even after UFD is set to false
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_unable_to_start_camera)
}
// Assert toast notification is still shown even after UFD is set to false
assertDisplayed(toastNotificationId)
assertDisplayed(toastNotificationIconId)
assertViewText(toastNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_unable_to_start_camera)
}
}

Просмотреть файл

@ -8,146 +8,162 @@ import com.azure.android.communication.assertDisplayed
import com.azure.android.communication.assertNotDisplayed
import com.azure.android.communication.assertViewText
import com.azure.android.communication.tap
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.tapWhenDisplayed
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.waitUntilDisplayed
import kotlinx.coroutines.test.runTest
import org.junit.Test
internal class UpperBarMessageNotificationTest : BaseUiTest() {
@Test
fun testNoSpeakerDevicesAvailableUpperBarMessageNotification() = runTest {
injectDependencies(testScheduler)
fun testNoSpeakerDevicesAvailableUpperBarMessageNotification() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.setNoSpeakerDevicesAvailable(true)
callingSDK.setNoSpeakerDevicesAvailable(true)
// Check that the upper message bar notification appeared
waitUntilDisplayed(upperMessageBarNotificationId)
// Check that the upper message bar notification appeared
waitUntilDisplayed(upperMessageBarNotificationId)
// Assert notification appears with correct text
assertDisplayed(upperMessageBarNotificationId)
assertDisplayed(upperMessageBarNotificationIconId)
assertViewText(upperMessageBarNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_unable_to_locate_speaker)
// Assert notification appears with correct text
assertDisplayed(upperMessageBarNotificationId)
assertDisplayed(upperMessageBarNotificationIconId)
assertViewText(
upperMessageBarNotificationMessageId,
R.string.azure_communication_ui_calling_diagnostics_unable_to_locate_speaker,
)
callingSDK.setNoSpeakerDevicesAvailable(false)
callingSDK.setNoSpeakerDevicesAvailable(false)
// Upper Bar Message Notification not present anymore due to UFD state change
assertNotDisplayed(upperMessageBarNotificationId)
// Upper Bar Message Notification not present anymore due to UFD state change
assertNotDisplayed(upperMessageBarNotificationId)
// Show the Upper Message Bar notification again and dismiss it using the X button
callingSDK.setNoSpeakerDevicesAvailable(true)
// Show the Upper Message Bar notification again and dismiss it using the X button
callingSDK.setNoSpeakerDevicesAvailable(true)
// Dismiss the notification pressing the X button
tap(upperMessageBarNotificationDismissButtonId)
// Dismiss the notification pressing the X button
tap(upperMessageBarNotificationDismissButtonId)
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
}
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
}
@Test
fun testNoMicrophoneDevicesAvailableUpperBarMessageNotification() = runTest {
injectDependencies(testScheduler)
fun testNoMicrophoneDevicesAvailableUpperBarMessageNotification() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.setNoMicrophoneDevicesAvailable(true)
callingSDK.setNoMicrophoneDevicesAvailable(true)
// Check that the upper message bar notification appeared
waitUntilDisplayed(upperMessageBarNotificationId)
// Check that the upper message bar notification appeared
waitUntilDisplayed(upperMessageBarNotificationId)
// Assert notification appears with correct text
assertDisplayed(upperMessageBarNotificationId)
assertDisplayed(upperMessageBarNotificationIconId)
assertViewText(upperMessageBarNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_unable_to_locate_microphone)
// Assert notification appears with correct text
assertDisplayed(upperMessageBarNotificationId)
assertDisplayed(upperMessageBarNotificationIconId)
assertViewText(
upperMessageBarNotificationMessageId,
R.string.azure_communication_ui_calling_diagnostics_unable_to_locate_microphone,
)
callingSDK.setNoMicrophoneDevicesAvailable(false)
callingSDK.setNoMicrophoneDevicesAvailable(false)
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
// Show the Upper Message Bar notification again and dismiss it using the X button
callingSDK.setNoMicrophoneDevicesAvailable(true)
// Show the Upper Message Bar notification again and dismiss it using the X button
callingSDK.setNoMicrophoneDevicesAvailable(true)
// Dismiss the notification pressing the X button
tap(upperMessageBarNotificationDismissButtonId)
// Dismiss the notification pressing the X button
tap(upperMessageBarNotificationDismissButtonId)
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
}
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
}
@Test
fun testMicrophoneNotFunctioningUpperBarMessageNotification() = runTest {
injectDependencies(testScheduler)
fun testMicrophoneNotFunctioningUpperBarMessageNotification() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.setMicrophoneNotFunctioning(true)
callingSDK.setMicrophoneNotFunctioning(true)
// Check that the upper message bar notification appeared
waitUntilDisplayed(upperMessageBarNotificationId)
// Check that the upper message bar notification appeared
waitUntilDisplayed(upperMessageBarNotificationId)
// Assert notification appears with correct text
assertDisplayed(upperMessageBarNotificationId)
assertDisplayed(upperMessageBarNotificationIconId)
assertViewText(upperMessageBarNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_microphone_not_working_as_expected)
// Assert notification appears with correct text
assertDisplayed(upperMessageBarNotificationId)
assertDisplayed(upperMessageBarNotificationIconId)
assertViewText(
upperMessageBarNotificationMessageId,
R.string.azure_communication_ui_calling_diagnostics_microphone_not_working_as_expected,
)
callingSDK.setMicrophoneNotFunctioning(false)
callingSDK.setMicrophoneNotFunctioning(false)
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
// Show the Upper Message Bar notification again and dismiss it using the X button
callingSDK.setMicrophoneNotFunctioning(true)
// Show the Upper Message Bar notification again and dismiss it using the X button
callingSDK.setMicrophoneNotFunctioning(true)
// Dismiss the notification pressing the X button
tap(upperMessageBarNotificationDismissButtonId)
// Dismiss the notification pressing the X button
tap(upperMessageBarNotificationDismissButtonId)
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
}
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
}
@Test
fun testSpeakerNotFunctioningUpperBarMessageNotification() = runTest {
injectDependencies(testScheduler)
fun testSpeakerNotFunctioningUpperBarMessageNotification() =
runTest {
injectDependencies(testScheduler)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
// Launch the UI.
launchComposite()
tapWhenDisplayed(joinCallId)
waitUntilDisplayed(endCallId)
callingSDK.setSpeakerNotFunctioning(true)
callingSDK.setSpeakerNotFunctioning(true)
// Check that the upper message bar notification appeared
waitUntilDisplayed(upperMessageBarNotificationId)
// Check that the upper message bar notification appeared
waitUntilDisplayed(upperMessageBarNotificationId)
// Assert notification appears with correct text
assertDisplayed(upperMessageBarNotificationId)
assertDisplayed(upperMessageBarNotificationIconId)
assertViewText(upperMessageBarNotificationMessageId, R.string.azure_communication_ui_calling_diagnostics_speaker_not_working_as_expected)
// Assert notification appears with correct text
assertDisplayed(upperMessageBarNotificationId)
assertDisplayed(upperMessageBarNotificationIconId)
assertViewText(
upperMessageBarNotificationMessageId,
R.string.azure_communication_ui_calling_diagnostics_speaker_not_working_as_expected,
)
callingSDK.setSpeakerNotFunctioning(false)
callingSDK.setSpeakerNotFunctioning(false)
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
// Show the Upper Message Bar notification again and dismiss it using the X button
callingSDK.setSpeakerNotFunctioning(true)
// Show the Upper Message Bar notification again and dismiss it using the X button
callingSDK.setSpeakerNotFunctioning(true)
// Dismiss the notification pressing the X button
tap(upperMessageBarNotificationDismissButtonId)
// Dismiss the notification pressing the X button
tap(upperMessageBarNotificationDismissButtonId)
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
}
// Upper Bar Message Notification not present anymore
assertNotDisplayed(upperMessageBarNotificationId)
}
}

Просмотреть файл

@ -24,7 +24,7 @@ internal fun launchComposite() {
CallCompositeRemoteOptions(
CallCompositeGroupCallLocator(UUID.fromString("74fce2c1-520f-11ec-97de-71411a9a8e14")),
communicationTokenCredential,
"test"
"test",
)
callComposite.launchTest(appContext, remoteOptions, null)

Просмотреть файл

@ -10,12 +10,14 @@ import com.azure.android.communication.ui.calling.presentation.manager.DebugInfo
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManagerImpl
import java.io.File
internal fun createDebugInfoManager(context: Context, getLogFiles: () -> List<File>): DebugInfoManager {
internal fun createDebugInfoManager(
context: Context,
getLogFiles: () -> List<File>,
): DebugInfoManager {
return DebugInfoManagerImpl(CallHistoryRepositoryImpl(context, DefaultLogger()), getLogFiles)
}
internal fun CallComposite.getDiContainer() =
CallComposite.diContainer
internal fun CallComposite.getDiContainer() = CallComposite.diContainer
internal fun CallComposite.onExit() {
CallComposite.diContainer = null

Просмотреть файл

@ -7,7 +7,6 @@ import com.azure.android.communication.ui.calling.configuration.CallCompositeCon
import java.lang.IllegalStateException
internal class CallCompositeInstanceManager {
/**
* CallCompositeInstance Storage
*
@ -25,7 +24,10 @@ internal class CallCompositeInstanceManager {
* Store a Config by Instance ID
*/
@JvmStatic
fun putCallComposite(id: Int, callComposite: CallComposite) {
fun putCallComposite(
id: Int,
callComposite: CallComposite,
) {
instances[id] = callComposite
}
@ -39,11 +41,12 @@ internal class CallCompositeInstanceManager {
* May return null if the Configuration becomes garbage collected
*/
@JvmStatic
fun getCallComposite(id: Int): CallComposite = instances[id]
?: throw CallCompositeException(
"This ID is not valid, and no entry exists in the map. Please file a bug, this is an error in the composite",
IllegalStateException()
)
fun getCallComposite(id: Int): CallComposite =
instances[id]
?: throw CallCompositeException(
"This ID is not valid, and no entry exists in the map. Please file a bug, this is an error in the composite",
IllegalStateException(),
)
/**
* Check if CallComposite exists

Просмотреть файл

@ -17,7 +17,7 @@ internal data class CallConfiguration(
val displayName: String,
val groupId: UUID?,
val meetingLink: String?,
val callType: CallType
val callType: CallType,
) {
val diagnosticConfig = DiagnosticConfig()
}

Просмотреть файл

@ -15,6 +15,7 @@ internal data class RemoteParticipantViewData(
internal interface RemoteParticipantsConfigurationHandler {
fun onSetParticipantViewData(data: RemoteParticipantViewData): CallCompositeSetParticipantViewDataResult
fun onRemoveParticipantViewData(identifier: String)
}
@ -31,7 +32,7 @@ internal class RemoteParticipantsConfiguration {
): CallCompositeSetParticipantViewDataResult {
handler?.get()?.let {
return@setParticipantViewData it.onSetParticipantViewData(
RemoteParticipantViewData(identifier, participantViewData)
RemoteParticipantViewData(identifier, participantViewData),
)
}
return CallCompositeSetParticipantViewDataResult.PARTICIPANT_NOT_IN_CALL

Просмотреть файл

@ -5,9 +5,9 @@ package com.azure.android.communication.ui.calling.configuration.events
import com.azure.android.communication.ui.calling.CallCompositeEventHandler
import com.azure.android.communication.ui.calling.models.CallCompositeCallStateChangedEvent
import com.azure.android.communication.ui.calling.models.CallCompositeDismissedEvent
import com.azure.android.communication.ui.calling.models.CallCompositeErrorEvent
import com.azure.android.communication.ui.calling.models.CallCompositePictureInPictureChangedEvent
import com.azure.android.communication.ui.calling.models.CallCompositeDismissedEvent
import com.azure.android.communication.ui.calling.models.CallCompositeRemoteParticipantJoinedEvent
import com.azure.android.communication.ui.calling.models.CallCompositeUserReportedIssueEvent
@ -27,11 +27,9 @@ internal class CallCompositeEventsHandler {
fun getOnErrorHandlers() = errorHandlers.asIterable()
fun addOnErrorEventHandler(errorHandler: CallCompositeEventHandler<CallCompositeErrorEvent>) =
errorHandlers.add(errorHandler)
fun addOnErrorEventHandler(errorHandler: CallCompositeEventHandler<CallCompositeErrorEvent>) = errorHandlers.add(errorHandler)
fun removeOnErrorEventHandler(errorHandler: CallCompositeEventHandler<CallCompositeErrorEvent>) =
errorHandlers.remove(errorHandler)
fun removeOnErrorEventHandler(errorHandler: CallCompositeEventHandler<CallCompositeErrorEvent>) = errorHandlers.remove(errorHandler)
fun getOnRemoteParticipantJoinedHandlers() = remoteParticipantJoinedHandlers.asIterable()
@ -42,6 +40,7 @@ internal class CallCompositeEventsHandler {
remoteParticipantJoinedHandlers.remove(handler)
fun getOnMultitaskingStateChangedEventHandlers() = multitaskingStateChangedEvent.asIterable()
fun addOnMultitaskingStateChangedEventHandler(handler: CallCompositeEventHandler<CallCompositePictureInPictureChangedEvent>) =
multitaskingStateChangedEvent.add(handler)

Просмотреть файл

@ -15,16 +15,22 @@ import org.threeten.bp.OffsetDateTime
import org.threeten.bp.ZoneId
internal interface CallHistoryRepository {
suspend fun insert(callId: String, callDateTime: OffsetDateTime)
suspend fun insert(
callId: String,
callDateTime: OffsetDateTime,
)
suspend fun getAll(): List<CallHistoryRecordData>
}
internal class CallHistoryRepositoryImpl(
private val context: Context,
private val logger: Logger
private val logger: Logger,
) : CallHistoryRepository {
override suspend fun insert(callId: String, callDateTime: OffsetDateTime) {
override suspend fun insert(
callId: String,
callDateTime: OffsetDateTime,
) {
return withContext(Dispatchers.IO) {
// SQLite does not allow concurrent writes. Need to queue them via lock.
synchronized(dbAccessLock) {
@ -32,10 +38,11 @@ internal class CallHistoryRepositoryImpl(
// reliable event when to dispose it.
DbHelper(context).writableDatabase
.use { db ->
val values = ContentValues().apply {
put(CallHistoryContract.COLUMN_NAME_CALL_ID, callId)
put(CallHistoryContract.COLUMN_NAME_CALL_DATE, callDateTime.toInstant().toEpochMilli())
}
val values =
ContentValues().apply {
put(CallHistoryContract.COLUMN_NAME_CALL_ID, callId)
put(CallHistoryContract.COLUMN_NAME_CALL_DATE, callDateTime.toInstant().toEpochMilli())
}
val result = db.insert(CallHistoryContract.TABLE_NAME, null, values)
if (result == -1L) {
@ -64,7 +71,7 @@ internal class CallHistoryRepositoryImpl(
"${CallHistoryContract.COLUMN_NAME_CALL_DATE}, " +
"${CallHistoryContract.COLUMN_NAME_CALL_ID} " +
"FROM ${CallHistoryContract.TABLE_NAME}",
null
null,
).use {
if (it.moveToFirst()) {
val idColumnIndex = it.getColumnIndexOrThrow(CallHistoryContract.COLUMN_NAME_ID)
@ -75,10 +82,12 @@ internal class CallHistoryRepositoryImpl(
CallHistoryRecordData(
id = it.getInt(idColumnIndex),
callId = it.getString(nameColumnIndex),
callStartedOn = OffsetDateTime.ofInstant(
Instant.ofEpochMilli(it.getLong(dateColumnIndex)), ZoneId.systemDefault()
),
)
callStartedOn =
OffsetDateTime.ofInstant(
Instant.ofEpochMilli(it.getLong(dateColumnIndex)),
ZoneId.systemDefault(),
),
),
)
} while (it.moveToNext())
}
@ -92,7 +101,8 @@ internal class CallHistoryRepositoryImpl(
val threshold = OffsetDateTime.now().minusDays(31).toInstant().toEpochMilli()
db.delete(
CallHistoryContract.TABLE_NAME,
"${CallHistoryContract.COLUMN_NAME_CALL_DATE} < ?", arrayOf(threshold.toString())
"${CallHistoryContract.COLUMN_NAME_CALL_DATE} < ?",
arrayOf(threshold.toString()),
)
}

Просмотреть файл

@ -14,10 +14,18 @@ internal class DbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_N
db.execSQL(CallHistoryContract.SQL_CREATE_CALL_HISTORY_INDEX)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
override fun onUpgrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int,
) {
}
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
override fun onDowngrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int,
) {
}
companion object {
@ -28,7 +36,6 @@ internal class DbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_N
}
internal object CallHistoryContract {
const val COLUMN_NAME_ID = BaseColumns._ID
const val TABLE_NAME = "call_history"

Просмотреть файл

@ -20,13 +20,13 @@ import com.azure.android.communication.ui.calling.presentation.manager.AvatarVie
import com.azure.android.communication.ui.calling.presentation.manager.CompositeExitManager
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManager
import com.azure.android.communication.ui.calling.presentation.manager.LifecycleManager
import com.azure.android.communication.ui.calling.presentation.manager.MultitaskingManager
import com.azure.android.communication.ui.calling.presentation.manager.NetworkManager
import com.azure.android.communication.ui.calling.presentation.manager.PermissionManager
import com.azure.android.communication.ui.calling.presentation.navigation.NavigationRouter
import com.azure.android.communication.ui.calling.redux.Store
import com.azure.android.communication.ui.calling.redux.middleware.handler.CallingMiddlewareActionHandler
import com.azure.android.communication.ui.calling.redux.state.ReduxState
import com.azure.android.communication.ui.calling.presentation.manager.MultitaskingManager
import com.azure.android.communication.ui.calling.service.CallHistoryService
import com.azure.android.communication.ui.calling.service.CallingService
import com.azure.android.communication.ui.calling.service.NotificationService

Просмотреть файл

@ -22,18 +22,18 @@ import com.azure.android.communication.ui.calling.presentation.manager.AudioFocu
import com.azure.android.communication.ui.calling.presentation.manager.AudioModeManager
import com.azure.android.communication.ui.calling.presentation.manager.AudioSessionManager
import com.azure.android.communication.ui.calling.presentation.manager.AvatarViewManager
import com.azure.android.communication.ui.calling.presentation.manager.CompositeExitManager
import com.azure.android.communication.ui.calling.presentation.manager.CameraStatusHook
import com.azure.android.communication.ui.calling.presentation.manager.CompositeExitManager
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManager
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManagerImpl
import com.azure.android.communication.ui.calling.presentation.manager.LifecycleManagerImpl
import com.azure.android.communication.ui.calling.presentation.manager.MeetingJoinedHook
import com.azure.android.communication.ui.calling.presentation.manager.MicStatusHook
import com.azure.android.communication.ui.calling.presentation.manager.MultitaskingManager
import com.azure.android.communication.ui.calling.presentation.manager.NetworkManager
import com.azure.android.communication.ui.calling.presentation.manager.ParticipantAddedOrRemovedHook
import com.azure.android.communication.ui.calling.presentation.manager.PermissionManager
import com.azure.android.communication.ui.calling.presentation.manager.SwitchCameraStatusHook
import com.azure.android.communication.ui.calling.presentation.navigation.NavigationRouterImpl
import com.azure.android.communication.ui.calling.redux.AppStore
import com.azure.android.communication.ui.calling.redux.Middleware
@ -49,14 +49,13 @@ import com.azure.android.communication.ui.calling.redux.reducer.LocalParticipant
import com.azure.android.communication.ui.calling.redux.reducer.NavigationReducerImpl
import com.azure.android.communication.ui.calling.redux.reducer.ParticipantStateReducerImpl
import com.azure.android.communication.ui.calling.redux.reducer.PermissionStateReducerImpl
import com.azure.android.communication.ui.calling.redux.reducer.PipReducerImpl
import com.azure.android.communication.ui.calling.redux.reducer.Reducer
import com.azure.android.communication.ui.calling.redux.state.AppReduxState
import com.azure.android.communication.ui.calling.redux.state.ReduxState
import com.azure.android.communication.ui.calling.service.CallingService
import com.azure.android.communication.ui.calling.presentation.manager.MultitaskingManager
import com.azure.android.communication.ui.calling.redux.reducer.PipReducerImpl
import com.azure.android.communication.ui.calling.service.CallHistoryService
import com.azure.android.communication.ui.calling.service.CallHistoryServiceImpl
import com.azure.android.communication.ui.calling.service.CallingService
import com.azure.android.communication.ui.calling.service.NotificationService
import com.azure.android.communication.ui.calling.service.sdk.CallingSDK
import com.azure.android.communication.ui.calling.service.sdk.CallingSDKEventHandler
@ -72,7 +71,6 @@ internal class DependencyInjectionContainerImpl(
private val customVideoStreamRendererFactory: VideoStreamRendererFactory?,
private val customCoroutineContextProvider: CoroutineContextProvider?,
) : DependencyInjectionContainer {
override var callCompositeActivityWeakReference: WeakReference<CallCompositeActivity> = WeakReference(null)
override val configuration by lazy {
@ -86,7 +84,7 @@ internal class DependencyInjectionContainerImpl(
override val callingMiddlewareActionHandler by lazy {
CallingMiddlewareActionHandlerImpl(
callingService,
coroutineContextProvider
coroutineContextProvider,
)
}
@ -102,7 +100,7 @@ internal class DependencyInjectionContainerImpl(
VideoViewManager(
callingSDKWrapper,
applicationContext,
customVideoStreamRendererFactory ?: VideoStreamRendererFactoryImpl()
customVideoStreamRendererFactory ?: VideoStreamRendererFactoryImpl(),
)
}
@ -151,7 +149,7 @@ internal class DependencyInjectionContainerImpl(
override val callHistoryService: CallHistoryService by lazy {
CallHistoryServiceImpl(
appStore,
callHistoryRepository
callHistoryRepository,
)
}
@ -160,7 +158,7 @@ internal class DependencyInjectionContainerImpl(
coroutineContextProvider,
appStore,
configuration.callCompositeLocalOptions,
configuration.remoteParticipantsConfiguration
configuration.remoteParticipantsConfiguration,
)
}
@ -173,7 +171,7 @@ internal class DependencyInjectionContainerImpl(
ParticipantAddedOrRemovedHook(),
MicStatusHook(),
SwitchCameraStatusHook(),
)
),
)
}
@ -190,7 +188,7 @@ internal class DependencyInjectionContainerImpl(
initialState,
appReduxStateReducer,
appMiddleware,
storeDispatcher
storeDispatcher,
)
}
@ -217,7 +215,7 @@ internal class DependencyInjectionContainerImpl(
configuration.callConfig?.displayName,
localOptions?.isCameraOn == true,
localOptions?.isMicrophoneOn == true,
localOptions?.audioVideoMode ?: CallCompositeAudioVideoMode.AUDIO_AND_VIDEO
localOptions?.audioVideoMode ?: CallCompositeAudioVideoMode.AUDIO_AND_VIDEO,
)
}
@ -239,7 +237,7 @@ internal class DependencyInjectionContainerImpl(
private val callingMiddleware: Middleware<ReduxState> by lazy {
CallingMiddlewareImpl(
callingMiddlewareActionHandler,
logger
logger,
)
}
@ -254,7 +252,7 @@ internal class DependencyInjectionContainerImpl(
navigationReducer,
audioSessionReducer,
pipReducer,
callDiagnosticsReducer
callDiagnosticsReducer,
) as Reducer<ReduxState>
}
//endregion
@ -269,7 +267,7 @@ internal class DependencyInjectionContainerImpl(
?: CallingSDKWrapper(
applicationContext,
callingSDKEventHandler,
configuration.callConfig
configuration.callConfig,
)
}

Просмотреть файл

@ -4,5 +4,5 @@ import com.azure.android.communication.ui.calling.models.CallCompositeEventCode
internal class CallStateError(
val errorCode: ErrorCode,
val callCompositeEventCode: CallCompositeEventCode? = null
val callCompositeEventCode: CallCompositeEventCode? = null,
)

Просмотреть файл

@ -34,24 +34,24 @@ import com.azure.android.communication.ui.calling.features.interfaces.SupportFil
*/
internal class ACSFeatureFactory
/**
* Constructor used by the factory
*/
private constructor() {
companion object {
/**
* Return and instance of the required feature
*
* @param feature interface of the required feature
* @param <F> type of the feature interface
* @return requested interface feature
</F> */
inline fun <reified F : ACSFeature> getFeature(): F {
return featureList[F::class.java]!! as F
}
* Constructor used by the factory
*/
private constructor() {
companion object {
/**
* Return and instance of the required feature
*
* @param feature interface of the required feature
* @param <F> type of the feature interface
* @return requested interface feature
</F> */
inline fun <reified F : ACSFeature> getFeature(): F {
return featureList[F::class.java]!! as F
}
private val featureList: Map<Class<*>, ACSFeature> =
mapOf(
SupportFilesFeature::class.java to SupportFilesFeatureImpl()
)
private val featureList: Map<Class<*>, ACSFeature> =
mapOf(
SupportFilesFeature::class.java to SupportFilesFeatureImpl(),
)
}
}
}

Просмотреть файл

@ -9,5 +9,8 @@ import com.azure.android.communication.ui.calling.features.ACSFeature
import java.io.File
abstract class SupportFilesFeature : ACSFeature {
abstract fun getSupportFiles(client: CallClient, context: Context): List<File>
abstract fun getSupportFiles(
client: CallClient,
context: Context,
): List<File>
}

Просмотреть файл

@ -4,8 +4,8 @@
package com.azure.android.communication.ui.calling.handlers
import com.azure.android.communication.ui.calling.configuration.CallCompositeConfiguration
import com.azure.android.communication.ui.calling.models.CallCompositeCallStateCode
import com.azure.android.communication.ui.calling.models.CallCompositeCallStateChangedEvent
import com.azure.android.communication.ui.calling.models.CallCompositeCallStateCode
import com.azure.android.communication.ui.calling.redux.Store
import com.azure.android.communication.ui.calling.redux.state.CallingStatus
import com.azure.android.communication.ui.calling.redux.state.ReduxState

Просмотреть файл

@ -51,9 +51,10 @@ internal class RemoteParticipantHandler(
try {
if (joinedParticipant.isNotEmpty()) {
val participantIdMap = remoteParticipantsCollection.getRemoteParticipantsMap()
val identifiers = joinedParticipant.map {
participantIdMap.getValue(it).identifier.into()
}
val identifiers =
joinedParticipant.map {
participantIdMap.getValue(it).identifier.into()
}
val eventArgs = CallCompositeRemoteParticipantJoinedEvent(identifiers)
configuration.callCompositeEventsHandler.getOnRemoteParticipantJoinedHandlers()
.forEach { it.handle(eventArgs) }

Просмотреть файл

@ -6,7 +6,6 @@ package com.azure.android.communication.ui.calling.logger
import android.util.Log
internal class DefaultLogger : Logger {
private val tag = "communication.ui"
override fun info(message: String) {
@ -21,7 +20,10 @@ internal class DefaultLogger : Logger {
Log.w(tag, message)
}
override fun error(message: String, error: Throwable?) {
override fun error(
message: String,
error: Throwable?,
) {
if (error != null) {
Log.e(tag, message, error)
} else {

Просмотреть файл

@ -5,7 +5,13 @@ package com.azure.android.communication.ui.calling.logger
internal interface Logger {
fun info(message: String)
fun debug(message: String)
fun warning(message: String)
fun error(message: String, error: Throwable? = null)
fun error(
message: String,
error: Throwable? = null,
)
}

Просмотреть файл

@ -6,9 +6,14 @@ package com.azure.android.communication.ui.calling.models
import org.threeten.bp.OffsetDateTime
import java.io.File
internal fun buildCallCompositeDebugInfo(callHistoryRecordList: List<CallCompositeCallHistoryRecord>, getLogFiles: () -> List<File>) =
CallCompositeDebugInfo(callHistoryRecordList, getLogFiles)
internal fun buildCallCompositeDebugInfo(
callHistoryRecordList: List<CallCompositeCallHistoryRecord>,
getLogFiles: () -> List<File>,
) = CallCompositeDebugInfo(callHistoryRecordList, getLogFiles)
internal fun buildCallHistoryRecord(callStartedOn: OffsetDateTime, callIds: List<String>): CallCompositeCallHistoryRecord {
internal fun buildCallHistoryRecord(
callStartedOn: OffsetDateTime,
callIds: List<String>,
): CallCompositeCallHistoryRecord {
return CallCompositeCallHistoryRecord(callStartedOn, callIds)
}

Просмотреть файл

@ -9,5 +9,5 @@ internal enum class CallCompositeInternalParticipantRole {
CONSUMER,
PRESENTER,
ORGANIZER,
COORGANIZER;
COORGANIZER,
}

Просмотреть файл

@ -8,5 +8,5 @@ internal enum class CallCompositeLobbyErrorCode {
LOBBY_CONVERSATION_TYPE_NOT_SUPPORTED,
LOBBY_MEETING_ROLE_NOT_ALLOWED,
REMOVE_PARTICIPANT_OPERATION_FAILURE,
UNKNOWN_ERROR
UNKNOWN_ERROR,
}

Просмотреть файл

@ -8,7 +8,7 @@ internal enum class NetworkCallDiagnostic {
NETWORK_RECEIVE_QUALITY,
NETWORK_SEND_QUALITY,
NETWORK_UNAVAILABLE,
NETWORK_RELAYS_UNREACHABLE
NETWORK_RELAYS_UNREACHABLE,
}
internal enum class MediaCallDiagnostic {
@ -25,19 +25,19 @@ internal enum class MediaCallDiagnostic {
CAMERA_FROZEN,
CAMERA_START_FAILED,
CAMERA_START_TIMED_OUT,
CAMERA_PERMISSION_DENIED
CAMERA_PERMISSION_DENIED,
}
internal enum class CallDiagnosticQuality {
UNKNOWN,
GOOD,
POOR,
BAD;
BAD,
}
internal data class CallDiagnosticModel<DiagnosticKind, DiagnosticValue>(
val diagnosticKind: DiagnosticKind,
val diagnosticValue: DiagnosticValue
val diagnosticValue: DiagnosticValue,
)
// Alias types representing different types of Call Diagnostics

Просмотреть файл

@ -9,7 +9,6 @@ internal data class ToastNotificationModel(
val networkCallDiagnostic: NetworkCallDiagnostic?,
val mediaCallDiagnostic: MediaCallDiagnostic?,
) {
fun isEmpty(): Boolean {
return notificationIconId == 0 && notificationMessageId == 0
}

Просмотреть файл

@ -6,7 +6,7 @@ package com.azure.android.communication.ui.calling.models
internal data class UpperMessageBarNotificationModel(
val notificationIconId: Int,
val notificationMessageId: Int,
val mediaCallDiagnostic: MediaCallDiagnostic?
val mediaCallDiagnostic: MediaCallDiagnostic?,
) {
fun isEmpty(): Boolean {
return notificationIconId == 0 && notificationMessageId == 0

Просмотреть файл

@ -27,8 +27,8 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.lifecycleScope
import com.azure.android.communication.ui.calling.CallCompositeException
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.ui.calling.CallCompositeInstanceManager
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.ui.calling.models.CallCompositeSupportedLocale
import com.azure.android.communication.ui.calling.models.CallCompositeSupportedScreenOrientation
import com.azure.android.communication.ui.calling.models.CallCompositeUserReportedIssueEvent
@ -126,7 +126,7 @@ internal open class CallCompositeActivity : AppCompatActivity() {
this,
getAudioPermissionLauncher(),
getCameraPermissionLauncher(),
lifecycleScope
lifecycleScope,
)
audioSessionManager.onCreate(savedInstanceState)
@ -152,7 +152,7 @@ internal open class CallCompositeActivity : AppCompatActivity() {
store.dispatch(PipAction.HideEntered())
}
}
}
},
)
// Probably can follow the above pattern now with function declarations
@ -189,8 +189,11 @@ internal open class CallCompositeActivity : AppCompatActivity() {
activity?.packageManager?.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) == true
) {
store.dispatch(
if (isInPictureInPictureMode) PipAction.PipModeEntered()
else PipAction.ShowNormalEntered()
if (isInPictureInPictureMode) {
PipAction.PipModeEntered()
} else {
PipAction.ShowNormalEntered()
},
)
}
}
@ -241,13 +244,15 @@ internal open class CallCompositeActivity : AppCompatActivity() {
activity?.packageManager?.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) == true &&
store.getCurrentState().navigationState.navigationState == NavigationStatus.IN_CALL
) {
val params = PictureInPictureParams
.Builder()
.setAspectRatio(Rational(1, 1))
.build()
val params =
PictureInPictureParams
.Builder()
.setAspectRatio(Rational(1, 1))
.build()
if (enterPictureInPictureMode(params))
if (enterPictureInPictureMode(params)) {
reduxStartPipMode()
}
}
} catch (_: Exception) {
// on some samsung devices(API 26) enterPictureInPictureMode crashes even FEATURE_PICTURE_IN_PICTURE is true
@ -257,7 +262,7 @@ internal open class CallCompositeActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
newConfig: Configuration,
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
store.dispatch(if (isInPictureInPictureMode) PipAction.PipModeEntered() else PipAction.ShowNormalEntered())
@ -284,28 +289,31 @@ internal open class CallCompositeActivity : AppCompatActivity() {
}
fun hide() {
if (!configuration.enableMultitasking)
if (!configuration.enableMultitasking) {
return
}
// TODO: should we enter PiP if we are on the setup screen?
if (configuration.enableSystemPiPWhenMultitasking &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
activity?.packageManager?.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) == true
) {
val params = PictureInPictureParams
.Builder()
.setAspectRatio(Rational(1, 1))
.build()
val params =
PictureInPictureParams
.Builder()
.setAspectRatio(Rational(1, 1))
.build()
var enteredPiPSucceeded = false
try {
enteredPiPSucceeded = enterPictureInPictureMode(params)
} catch (_: Exception) {
// on some samsung devices(API 26) enterPictureInPictureMode crashes even FEATURE_PICTURE_IN_PICTURE is true
}
if (enteredPiPSucceeded)
if (enteredPiPSucceeded) {
reduxStartPipMode()
else
} else {
activity?.moveTaskToBack(true)
}
} else {
activity?.moveTaskToBack(true)
}
@ -317,9 +325,9 @@ internal open class CallCompositeActivity : AppCompatActivity() {
ColorDrawable(
ContextCompat.getColor(
this,
R.color.azure_communication_ui_calling_color_background
)
)
R.color.azure_communication_ui_calling_color_background,
),
),
)
supportActionBar?.setHomeAsUpIndicator(R.drawable.azure_communication_ui_calling_ic_fluent_arrow_left_24_filled)
}
@ -332,7 +340,7 @@ internal open class CallCompositeActivity : AppCompatActivity() {
container.configuration.callCompositeEventsHandler.getOnUserReportedHandlers().forEach {
try {
it.handle(
event
event,
)
} catch (e: Exception) {
// Ignore any exception from the user handler
@ -342,18 +350,19 @@ internal open class CallCompositeActivity : AppCompatActivity() {
private fun configureLocalization() {
val config: Configuration = resources.configuration
val locale = when (configuration.localizationConfig) {
null -> {
supportedOSLocale()
}
else -> {
configuration.localizationConfig!!.layoutDirection?.let {
window?.decorView?.layoutDirection = it
val locale =
when (configuration.localizationConfig) {
null -> {
supportedOSLocale()
}
else -> {
configuration.localizationConfig!!.layoutDirection?.let {
window?.decorView?.layoutDirection = it
}
configuration.localizationConfig!!.locale
}
configuration.localizationConfig!!.locale
}
}
config.setLocale(locale)
resources.updateConfiguration(config, resources.displayMetrics)
@ -372,7 +381,7 @@ internal open class CallCompositeActivity : AppCompatActivity() {
private fun getCameraPermissionLauncher(): ActivityResultLauncher<String> {
return registerForActivityResult(
ActivityResultContracts.RequestPermission()
ActivityResultContracts.RequestPermission(),
) {
permissionManager.setCameraPermissionsState()
}
@ -380,7 +389,7 @@ internal open class CallCompositeActivity : AppCompatActivity() {
private fun getAudioPermissionLauncher(): ActivityResultLauncher<Array<String>> {
return registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
ActivityResultContracts.RequestMultiplePermissions(),
) {
permissionManager.setAudioPermissionsState()
}
@ -446,10 +455,11 @@ internal open class CallCompositeActivity : AppCompatActivity() {
}
}
val fragment = supportFragmentManager.fragmentFactory.instantiate(
classLoader,
fragmentClassName
)
val fragment =
supportFragmentManager.fragmentFactory.instantiate(
classLoader,
fragmentClassName,
)
val transaction: FragmentTransaction = supportFragmentManager.beginTransaction()
// For accessibility, we are going to turn it off during the transaction
@ -469,18 +479,20 @@ internal open class CallCompositeActivity : AppCompatActivity() {
private fun setStatusBarColor() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
window.statusBarColor = ContextCompat.getColor(
this,
R.color.azure_communication_ui_calling_color_status_bar
)
val isNightMode = this.resources.configuration.uiMode
.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
window.statusBarColor =
ContextCompat.getColor(
this,
R.color.azure_communication_ui_calling_color_status_bar,
)
val isNightMode =
this.resources.configuration.uiMode
.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
if (isNightMode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.setSystemBarsAppearance(
0,
APPEARANCE_LIGHT_STATUS_BARS
APPEARANCE_LIGHT_STATUS_BARS,
)
} else {
window.clearFlags(0)
@ -489,7 +501,7 @@ internal open class CallCompositeActivity : AppCompatActivity() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.setSystemBarsAppearance(
APPEARANCE_LIGHT_STATUS_BARS,
APPEARANCE_LIGHT_STATUS_BARS
APPEARANCE_LIGHT_STATUS_BARS,
)
} else {
@Suppress("DEPRECATION")

Просмотреть файл

@ -5,11 +5,11 @@ package com.azure.android.communication.ui.calling.presentation
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.ui.calling.CallCompositeException
import com.azure.android.communication.ui.calling.CallCompositeInstanceManager
import com.azure.android.communication.ui.calling.di.DependencyInjectionContainer
import com.azure.android.communication.ui.calling.getDiContainer
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.ui.calling.models.CallCompositeAudioVideoMode
import com.azure.android.communication.ui.calling.presentation.fragment.calling.CallingViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.factories.CallingViewModelFactory
@ -33,6 +33,7 @@ internal class DependencyInjectionContainerHolder(
private const val commonMessage =
"Please ensure that you have set a valid instanceId before retrieving the container."
}
// Instance ID to locate Configuration. -1 is invalid.
var instanceId: Int = -1
set(value) {
@ -58,7 +59,7 @@ internal class DependencyInjectionContainerHolder(
SetupViewModel(
container.appStore,
SetupViewModelFactory(container.appStore, application),
container.networkManager
container.networkManager,
)
}
val callingViewModel by lazy {
@ -70,12 +71,12 @@ internal class DependencyInjectionContainerHolder(
application.resources.getInteger(R.integer.azure_communication_ui_calling_max_remote_participants),
container.debugInfoManager,
container.configuration.callCompositeEventsHandler.getOnUserReportedHandlers().toList().isNotEmpty(),
container.configuration.enableMultitasking
container.configuration.enableMultitasking,
),
container.networkManager,
container.configuration.enableMultitasking,
container.configuration.callCompositeLocalOptions?.audioVideoMode
?: CallCompositeAudioVideoMode.AUDIO_AND_VIDEO
?: CallCompositeAudioVideoMode.AUDIO_AND_VIDEO,
)
}
}

Просмотреть файл

@ -6,16 +6,16 @@ package com.azure.android.communication.ui.calling.presentation
import android.content.Context
import android.view.View
import android.view.ViewGroup
import com.azure.android.communication.calling.CreateViewOptions
import com.azure.android.communication.calling.MediaStreamType
import com.azure.android.communication.calling.ScalingMode
import com.azure.android.communication.calling.CreateViewOptions
import com.azure.android.communication.ui.calling.service.sdk.CallingSDK
import com.azure.android.communication.ui.calling.service.sdk.LocalVideoStream
import com.azure.android.communication.ui.calling.service.sdk.RemoteVideoStream
import com.azure.android.communication.ui.calling.service.sdk.VideoStreamRenderer
import com.azure.android.communication.ui.calling.service.sdk.VideoStreamRendererLocalWrapper
import com.azure.android.communication.ui.calling.service.sdk.VideoStreamRendererView
import com.azure.android.communication.ui.calling.service.sdk.VideoStreamRendererRemoteWrapper
import com.azure.android.communication.ui.calling.service.sdk.VideoStreamRendererView
import com.azure.android.communication.ui.calling.utilities.isAndroidTV
internal class VideoViewManager(
@ -65,9 +65,7 @@ internal class VideoViewManager(
localParticipantVideoRendererMap.clear()
}
fun removeRemoteParticipantVideoRenderer(
userVideoStreams: List<Pair<String, String>>,
) {
fun removeRemoteParticipantVideoRenderer(userVideoStreams: List<Pair<String, String>>) {
removeRemoteParticipantRenderer(userVideoStreams)
val remoteParticipants = callingSDKWrapper.getRemoteParticipantsMap()
@ -92,7 +90,7 @@ internal class VideoViewManager(
val videoStreamRenderer =
videoStreamRendererFactory.getLocalParticipantVideoStreamRenderer(
videoStream,
context
context,
)
val rendererView = videoStreamRenderer.createView()
localParticipantVideoRendererMap[videoStreamID] =
@ -102,7 +100,10 @@ internal class VideoViewManager(
}
}
fun getLocalVideoRenderer(videoStreamID: String, scalingMode: ScalingMode): View? {
fun getLocalVideoRenderer(
videoStreamID: String,
scalingMode: ScalingMode,
): View? {
var rendererView: VideoStreamRendererView? = null
if (localParticipantVideoRendererMap.containsKey(videoStreamID)) {
rendererView = localParticipantVideoRendererMap[videoStreamID]?.rendererView
@ -125,7 +126,7 @@ internal class VideoViewManager(
} else if (updateRemoteParticipantVideoRenderer(
participantID,
videoStreamId,
context
context,
)
) {
rendererView = remoteParticipantVideoRendererMap[uniqueID]?.rendererView
@ -146,37 +147,41 @@ internal class VideoViewManager(
val uniqueID = generateUniqueKey(userID, videoStreamID)
if (!remoteParticipantVideoRendererMap.containsKey(uniqueID)) {
if (remoteParticipants.containsKey(userID) &&
remoteParticipants[userID]?.videoStreams != null &&
remoteParticipants[userID]?.videoStreams?.size!! > 0
) {
val stream = remoteParticipants[userID]?.videoStreams?.find { videoStream ->
videoStream.id.toString() == videoStreamID
}
val stream =
remoteParticipants[userID]?.videoStreams?.find { videoStream ->
videoStream.id.toString() == videoStreamID
}
if (stream != null) {
val isScreenShare = stream.mediaStreamType == MediaStreamType.SCREEN_SHARING
val videoStreamRenderer =
videoStreamRendererFactory.getRemoteParticipantVideoStreamRenderer(
stream,
context
context,
)
val forceFitMode = isAndroidTV && !isScreenShare && remoteParticipants.size <= 1
val rendererView =
if (isScreenShare || forceFitMode) videoStreamRenderer.createView(
CreateViewOptions(
ScalingMode.FIT
if (isScreenShare || forceFitMode) {
videoStreamRenderer.createView(
CreateViewOptions(
ScalingMode.FIT,
),
)
) else videoStreamRenderer.createView()
} else {
videoStreamRenderer.createView()
}
remoteParticipantVideoRendererMap[uniqueID] =
VideoRenderer(
rendererView,
videoStreamRenderer,
isScreenShare
isScreenShare,
)
return true
}
@ -187,9 +192,10 @@ internal class VideoViewManager(
}
private fun removeLocalParticipantRenderer(videoStreamID: String?) {
val removedLocalStreams = localParticipantVideoRendererMap.filter { (streamID, _) ->
streamID !== videoStreamID
}
val removedLocalStreams =
localParticipantVideoRendererMap.filter { (streamID, _) ->
streamID !== videoStreamID
}
removedLocalStreams.values.forEach { videoStream ->
destroyVideoRenderer(videoStream)
@ -205,9 +211,10 @@ internal class VideoViewManager(
userWithVideoStreamList.forEach { (userID, streamID) ->
uniqueIDStreamList = uniqueIDStreamList.plus(generateUniqueKey(userID, streamID))
}
val removedRemoteStreams = remoteParticipantVideoRendererMap.filter { (streamID, _) ->
streamID !in uniqueIDStreamList
}
val removedRemoteStreams =
remoteParticipantVideoRendererMap.filter { (streamID, _) ->
streamID !in uniqueIDStreamList
}
removedRemoteStreams.values.forEach { videoStream ->
destroyVideoRenderer(videoStream)
@ -218,7 +225,10 @@ internal class VideoViewManager(
}
}
private fun generateUniqueKey(userIdentifier: String, videoStreamId: String): String {
private fun generateUniqueKey(
userIdentifier: String,
videoStreamId: String,
): String {
return "$userIdentifier:$videoStreamId"
}
@ -255,18 +265,16 @@ internal class VideoStreamRendererFactoryImpl : VideoStreamRendererFactory {
override fun getRemoteParticipantVideoStreamRenderer(
stream: RemoteVideoStream,
context: Context,
) =
VideoStreamRendererRemoteWrapper(
stream.native as com.azure.android.communication.calling.RemoteVideoStream,
context
)
) = VideoStreamRendererRemoteWrapper(
stream.native as com.azure.android.communication.calling.RemoteVideoStream,
context,
)
override fun getLocalParticipantVideoStreamRenderer(
stream: LocalVideoStream,
context: Context,
) =
VideoStreamRendererLocalWrapper(
stream.native as com.azure.android.communication.calling.LocalVideoStream,
context
)
) = VideoStreamRendererLocalWrapper(
stream.native as com.azure.android.communication.calling.LocalVideoStream,
context,
)
}

Просмотреть файл

@ -13,7 +13,6 @@ import kotlinx.coroutines.launch
internal abstract class BaseViewModel constructor(
protected val store: Store<ReduxState>,
) {
open fun init(coroutineScope: CoroutineScope) {
coroutineScope.launch {
store.getStateFlow().collect {

Просмотреть файл

@ -18,26 +18,26 @@ import androidx.activity.addCallback
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.ui.calling.CallCompositeInstanceManager
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.ui.calling.presentation.DependencyInjectionContainerHolder
import com.azure.android.communication.ui.calling.presentation.MultitaskingCallCompositeActivity
import com.azure.android.communication.ui.calling.presentation.fragment.calling.banner.BannerView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.controlbar.ControlBarView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.controlbar.more.MoreCallOptionsListView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.hangup.LeaveConfirmView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.header.InfoHeaderView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.hold.OnHoldOverlayView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.lobby.ConnectingLobbyOverlayView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.lobby.LobbyErrorHeaderView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.lobby.LobbyHeaderView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.lobby.WaitingLobbyOverlayView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.localuser.LocalParticipantView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.notification.ToastNotificationView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.notification.UpperMessageBarNotificationLayoutView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.participant.grid.ParticipantGridView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.participantlist.ParticipantListView
import com.azure.android.communication.ui.calling.presentation.fragment.common.audiodevicelist.AudioDeviceListView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.controlbar.more.MoreCallOptionsListView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.lobby.ConnectingLobbyOverlayView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.notification.ToastNotificationView
import com.azure.android.communication.ui.calling.presentation.fragment.calling.notification.UpperMessageBarNotificationLayoutView
import com.azure.android.communication.ui.calling.presentation.fragment.setup.components.ErrorInfoView
internal class CallingFragment :
@ -78,7 +78,10 @@ internal class CallingFragment :
private lateinit var lobbyHeaderView: LobbyHeaderView
private lateinit var lobbyErrorHeaderView: LobbyErrorHeaderView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
viewModel.init(viewLifecycleOwner.lifecycleScope)
@ -87,7 +90,7 @@ internal class CallingFragment :
confirmLeaveOverlayView.layoutDirection =
activity?.window?.decorView?.layoutDirection ?: LayoutDirection.LOCALE
confirmLeaveOverlayView.start(
viewLifecycleOwner
viewLifecycleOwner,
)
controlBarView = view.findViewById(R.id.azure_communication_ui_call_call_buttons)
@ -100,7 +103,7 @@ internal class CallingFragment :
videoViewManager,
viewLifecycleOwner,
this::switchFloatingHeader,
avatarViewManager
avatarViewManager,
)
connectingLobbyOverlay = view.findViewById(R.id.azure_communication_ui_call_connecting_lobby_overlay)
@ -127,33 +130,33 @@ internal class CallingFragment :
viewLifecycleOwner,
viewModel.floatingHeaderViewModel,
this::displayParticipantList,
accessibilityManager.isEnabled
accessibilityManager.isEnabled,
)
lobbyHeaderView = view.findViewById(R.id.azure_communication_ui_calling_lobby_header)
lobbyHeaderView.start(
viewLifecycleOwner,
viewModel.lobbyHeaderViewModel,
this::displayParticipantList
this::displayParticipantList,
)
lobbyErrorHeaderView = view.findViewById(R.id.azure_communication_ui_calling_lobby_error_header)
lobbyErrorHeaderView.start(
viewLifecycleOwner,
viewModel.lobbyErrorHeaderViewModel
viewModel.lobbyErrorHeaderViewModel,
)
upperMessageBarNotificationLayoutView = view.findViewById(R.id.azure_communication_ui_calling_upper_message_bar_notifications_layout)
upperMessageBarNotificationLayoutView.start(
viewLifecycleOwner,
viewModel.upperMessageBarNotificationLayoutViewModel,
accessibilityManager.isEnabled
accessibilityManager.isEnabled,
)
toastNotificationView = view.findViewById(R.id.azure_communication_ui_calling_toast_notification)
toastNotificationView.start(
viewLifecycleOwner,
viewModel.toastNotificationViewModel,
accessibilityManager.isEnabled
accessibilityManager.isEnabled,
)
audioDeviceListView =
@ -162,11 +165,12 @@ internal class CallingFragment :
activity?.window?.decorView?.layoutDirection ?: LayoutDirection.LOCALE
audioDeviceListView.start(viewLifecycleOwner)
participantListView = ParticipantListView(
viewModel.participantListViewModel,
this.requireContext(),
avatarViewManager,
)
participantListView =
ParticipantListView(
viewModel.participantListViewModel,
this.requireContext(),
avatarViewManager,
)
participantListView.layoutDirection =
activity?.window?.decorView?.layoutDirection ?: LayoutDirection.LOCALE
participantListView.start(viewLifecycleOwner)
@ -183,10 +187,11 @@ internal class CallingFragment :
errorInfoView = ErrorInfoView(view)
errorInfoView.start(viewLifecycleOwner, viewModel.errorInfoViewModel)
moreCallOptionsListView = MoreCallOptionsListView(
this.requireContext(),
viewModel.moreCallOptionsListViewModel
)
moreCallOptionsListView =
MoreCallOptionsListView(
this.requireContext(),
viewModel.moreCallOptionsListViewModel,
)
moreCallOptionsListView.layoutDirection =
activity?.window?.decorView?.layoutDirection ?: LayoutDirection.LOCALE
moreCallOptionsListView.start(viewLifecycleOwner)
@ -205,7 +210,6 @@ internal class CallingFragment :
}
private fun onBackPressed() {
if (viewModel.multitaskingEnabled) {
(activity as? MultitaskingCallCompositeActivity)?.hide()
} else {
@ -225,7 +229,7 @@ internal class CallingFragment :
sensorManager.registerListener(
this,
sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY),
SensorManager.SENSOR_DELAY_NORMAL
SensorManager.SENSOR_DELAY_NORMAL,
)
}
@ -265,7 +269,10 @@ internal class CallingFragment :
if (this::toastNotificationView.isInitialized) toastNotificationView.stop()
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
override fun onAccuracyChanged(
sensor: Sensor,
accuracy: Int,
) {}
override fun onSensorChanged(event: SensorEvent) {
if (event.sensor.type == Sensor.TYPE_PROXIMITY) {
@ -286,7 +293,7 @@ internal class CallingFragment :
mapOf(
LEAVE_CONFIRM_VIEW_KEY to viewModel.confirmLeaveOverlayViewModel.getShouldDisplayLeaveConfirmFlow(),
AUDIO_DEVICE_LIST_VIEW_KEY to viewModel.audioDeviceListViewModel.displayAudioDeviceSelectionMenuStateFlow,
PARTICIPANT_LIST_VIEW_KEY to viewModel.participantListViewModel.getDisplayParticipantListStateFlow()
PARTICIPANT_LIST_VIEW_KEY to viewModel.participantListViewModel.getDisplayParticipantListStateFlow(),
).forEach { (key, element) -> outState.putBoolean(key, element.value) }
super.onSaveInstanceState(outState)
}
@ -298,7 +305,7 @@ internal class CallingFragment :
mapOf(
LEAVE_CONFIRM_VIEW_KEY to viewModel.confirmLeaveOverlayViewModel::requestExitConfirmation,
AUDIO_DEVICE_LIST_VIEW_KEY to viewModel.audioDeviceListViewModel::displayAudioDeviceSelectionMenu,
PARTICIPANT_LIST_VIEW_KEY to viewModel.participantListViewModel::displayParticipantList
PARTICIPANT_LIST_VIEW_KEY to viewModel.participantListViewModel::displayParticipantList,
).forEach { (key, showDialog) -> if (it.getBoolean(key)) showDialog() }
}
}

Просмотреть файл

@ -27,7 +27,6 @@ internal class CallingViewModel(
val avMode: CallCompositeAudioVideoMode,
) :
BaseViewModel(store) {
val moreCallOptionsListViewModel = callingViewModelProvider.moreCallOptionsListViewModel
val participantGridViewModel = callingViewModelProvider.participantGridViewModel
val controlBarViewModel = callingViewModelProvider.controlBarViewModel
@ -80,7 +79,7 @@ internal class CallingViewModel(
state.localParticipantState.cameraState.device,
state.localParticipantState.cameraState.camerasCount,
state.pipState.status,
avMode
avMode,
)
floatingHeaderViewModel.init(
@ -90,16 +89,16 @@ internal class CallingViewModel(
)
audioDeviceListViewModel.init(
state.localParticipantState.audioState
state.localParticipantState.audioState,
)
bannerViewModel.init(
state.callState
state.callState,
)
participantListViewModel.init(
state.remoteParticipantState.participantMap,
state.localParticipantState,
canShowLobby(state.localParticipantState.localParticipantRole)
canShowLobby(state.localParticipantState.localParticipantRole),
)
waitingLobbyOverlayViewModel.init(state.callState.callingStatus)
@ -118,20 +117,19 @@ internal class CallingViewModel(
lobbyHeaderViewModel.init(
state.callState.callingStatus,
getLobbyParticipantsForHeader(state),
canShowLobby(state.localParticipantState.localParticipantRole)
canShowLobby(state.localParticipantState.localParticipantRole),
)
lobbyErrorHeaderViewModel.init(
state.callState.callingStatus,
state.remoteParticipantState.lobbyErrorCode,
canShowLobby(state.localParticipantState.localParticipantRole)
canShowLobby(state.localParticipantState.localParticipantRole),
)
super.init(coroutineScope)
}
override suspend fun onStateChange(state: ReduxState) {
if (!hasSetupCalled &&
state.callState.operationStatus == OperationStatus.SKIP_SETUP_SCREEN &&
state.permissionState.audioPermissionState == PermissionStatus.GRANTED
@ -165,7 +163,7 @@ internal class CallingViewModel(
state.localParticipantState.cameraState.device,
state.localParticipantState.cameraState.camerasCount,
state.pipState.status,
avMode
avMode,
)
audioDeviceListViewModel.update(
@ -204,7 +202,7 @@ internal class CallingViewModel(
state.localParticipantState.cameraState.device,
state.localParticipantState.cameraState.camerasCount,
state.pipState.status,
avMode
avMode,
)
}
@ -218,33 +216,33 @@ internal class CallingViewModel(
)
floatingHeaderViewModel.update(
remoteParticipantsForGridView.count()
remoteParticipantsForGridView.count(),
)
lobbyHeaderViewModel.update(
state.callState.callingStatus,
getLobbyParticipantsForHeader(state),
canShowLobby(state.localParticipantState.localParticipantRole)
canShowLobby(state.localParticipantState.localParticipantRole),
)
lobbyErrorHeaderViewModel.update(
state.callState.callingStatus,
state.remoteParticipantState.lobbyErrorCode,
canShowLobby(state.localParticipantState.localParticipantRole)
canShowLobby(state.localParticipantState.localParticipantRole),
)
upperMessageBarNotificationLayoutViewModel.update(
state.callDiagnosticsState
state.callDiagnosticsState,
)
toastNotificationViewModel.update(
state.callDiagnosticsState
state.callDiagnosticsState,
)
participantListViewModel.update(
state.remoteParticipantState.participantMap,
state.localParticipantState,
canShowLobby(state.localParticipantState.localParticipantRole)
canShowLobby(state.localParticipantState.localParticipantRole),
)
bannerViewModel.update(state.callState)
@ -257,9 +255,11 @@ internal class CallingViewModel(
}
private fun getLobbyParticipantsForHeader(state: ReduxState) =
if (canShowLobby(state.localParticipantState.localParticipantRole))
if (canShowLobby(state.localParticipantState.localParticipantRole)) {
state.remoteParticipantState.participantMap.filter { it.value.participantStatus == ParticipantStatus.IN_LOBBY }
else mapOf()
} else {
mapOf()
}
private fun canShowLobby(role: CallCompositeInternalParticipantRole?): Boolean {
role?.let {
@ -276,8 +276,7 @@ internal class CallingViewModel(
it.value.participantStatus != ParticipantStatus.IN_LOBBY
}
private fun shouldUpdateRemoteParticipantsViewModels(state: ReduxState) =
state.callState.callingStatus == CallingStatus.CONNECTED
private fun shouldUpdateRemoteParticipantsViewModels(state: ReduxState) = state.callState.callingStatus == CallingStatus.CONNECTED
private fun updateOverlayDisplayedState(callingStatus: CallingStatus) {
floatingHeaderViewModel.updateIsOverlayDisplayed(callingStatus)

Просмотреть файл

@ -16,7 +16,6 @@ import androidx.core.view.ViewCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.azure.android.communication.ui.calling.implementation.R
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
internal class BannerView : ConstraintLayout {
@ -69,19 +68,26 @@ internal class BannerView : ConstraintLayout {
bannerText.text = getBannerInfo(bannerInfoType)
bannerText.setOnClickListener(getBannerClickDestination(bannerInfoType))
val textToAnnounce = "${context.getString(R.string.azure_communication_ui_calling_alert_title)}: ${context.getString(R.string.azure_communication_ui_calling_view_button_close_button_full_accessibility_label)}, ${bannerText.text} ${context.getString(R.string.azure_communication_ui_calling_view_link)}"
val textToAnnounce = "${context.getString(
R.string.azure_communication_ui_calling_alert_title,
)}: ${context.getString(
R.string.azure_communication_ui_calling_view_button_close_button_full_accessibility_label,
)}, ${bannerText.text} ${context.getString(R.string.azure_communication_ui_calling_view_link)}"
announceForAccessibility(textToAnnounce)
bannerCloseButton.contentDescription = "${getBannerTitle(bannerText.text)}: ${context.getString(R.string.azure_communication_ui_calling_view_button_close_button_accessibility_label)}"
}
// Below code helps to display banner message on screen rotate. When recording and transcription being saved is displayed
// and screen is rotated, blank banner is displayed.
// We can not remove reset state in view model on stop as that cause incorrect message order
else if (bannerText.text.isNullOrBlank() && viewModel.displayedBannerType != BannerInfoType.BLANK) {
} else if (bannerText.text.isNullOrBlank() && viewModel.displayedBannerType != BannerInfoType.BLANK) {
// Below code helps to display banner message on screen rotate. When recording and transcription being saved is displayed
// and screen is rotated, blank banner is displayed.
// We can not remove reset state in view model on stop as that cause incorrect message order
bannerText.text = getBannerInfo(viewModel.displayedBannerType)
bannerText.setOnClickListener(getBannerClickDestination(bannerInfoType))
val textToAnnounce = "${context.getString(R.string.azure_communication_ui_calling_alert_title)}: ${context.getString(R.string.azure_communication_ui_calling_view_button_close_button_full_accessibility_label)}, ${bannerText.text} ${context.getString(R.string.azure_communication_ui_calling_view_link)}"
val textToAnnounce = "${context.getString(
R.string.azure_communication_ui_calling_alert_title,
)}: ${context.getString(
R.string.azure_communication_ui_calling_view_button_close_button_full_accessibility_label,
)}, ${bannerText.text} ${context.getString(R.string.azure_communication_ui_calling_view_link)}"
announceForAccessibility(textToAnnounce)
bannerCloseButton.contentDescription = "${getBannerTitle(bannerText.text)}: ${context.getString(R.string.azure_communication_ui_calling_view_button_close_button_accessibility_label)}"
@ -91,25 +97,26 @@ internal class BannerView : ConstraintLayout {
private fun getBannerClickDestination(bannerInfoType: BannerInfoType): OnClickListener {
return OnClickListener {
bannerText.isEnabled = false
val url = when (bannerInfoType) {
BannerInfoType.RECORDING_AND_TRANSCRIPTION_STARTED,
BannerInfoType.RECORDING_STARTED,
BannerInfoType.TRANSCRIPTION_STARTED,
BannerInfoType.RECORDING_STOPPED_STILL_TRANSCRIBING,
BannerInfoType.TRANSCRIPTION_STOPPED_STILL_RECORDING,
-> {
context.getString(R.string.azure_communication_ui_calling_view_link_privacy_policy_url)
val url =
when (bannerInfoType) {
BannerInfoType.RECORDING_AND_TRANSCRIPTION_STARTED,
BannerInfoType.RECORDING_STARTED,
BannerInfoType.TRANSCRIPTION_STARTED,
BannerInfoType.RECORDING_STOPPED_STILL_TRANSCRIBING,
BannerInfoType.TRANSCRIPTION_STOPPED_STILL_RECORDING,
-> {
context.getString(R.string.azure_communication_ui_calling_view_link_privacy_policy_url)
}
BannerInfoType.TRANSCRIPTION_STOPPED,
BannerInfoType.RECORDING_STOPPED,
BannerInfoType.RECORDING_AND_TRANSCRIPTION_STOPPED,
-> {
context.getString(R.string.azure_communication_ui_calling_view_link_learn_more_url)
}
else -> {
""
}
}
BannerInfoType.TRANSCRIPTION_STOPPED,
BannerInfoType.RECORDING_STOPPED,
BannerInfoType.RECORDING_AND_TRANSCRIPTION_STOPPED,
-> {
context.getString(R.string.azure_communication_ui_calling_view_link_learn_more_url)
}
else -> {
""
}
}
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
@ -118,7 +125,7 @@ internal class BannerView : ConstraintLayout {
{
bannerText.isEnabled = true
},
400
400,
)
}
}

Просмотреть файл

@ -8,7 +8,6 @@ import com.azure.android.communication.ui.calling.redux.state.CallingStatus
import kotlinx.coroutines.flow.MutableStateFlow
internal class BannerViewModel {
private var _bannerInfoTypeStateFlow: MutableStateFlow<BannerInfoType> =
MutableStateFlow(BannerInfoType.BLANK)
var bannerInfoTypeStateFlow = _bannerInfoTypeStateFlow
@ -28,12 +27,13 @@ internal class BannerViewModel {
}
fun init(callingState: CallingState) {
bannerInfoTypeStateFlow = MutableStateFlow(
createBannerInfoType(
callingState.isRecording,
callingState.isTranscribing
bannerInfoTypeStateFlow =
MutableStateFlow(
createBannerInfoType(
callingState.isRecording,
callingState.isTranscribing,
),
)
)
_isOverlayDisplayedFlow = MutableStateFlow(isOverlayDisplayed(callingState.callingStatus))
}
@ -66,27 +66,29 @@ internal class BannerViewModel {
isRecording: Boolean,
isTranscribing: Boolean,
): BannerInfoType {
recordingState = when (isRecording) {
true -> ComplianceState.ON
false -> {
if (recordingState == ComplianceState.ON) {
ComplianceState.STOPPED
} else {
recordingState
recordingState =
when (isRecording) {
true -> ComplianceState.ON
false -> {
if (recordingState == ComplianceState.ON) {
ComplianceState.STOPPED
} else {
recordingState
}
}
}
}
transcriptionState = when (isTranscribing) {
true -> ComplianceState.ON
false -> {
if (transcriptionState == ComplianceState.ON) {
ComplianceState.STOPPED
} else {
transcriptionState
transcriptionState =
when (isTranscribing) {
true -> ComplianceState.ON
false -> {
if (transcriptionState == ComplianceState.ON) {
ComplianceState.STOPPED
} else {
transcriptionState
}
}
}
}
if ((recordingState == ComplianceState.ON) &&
(transcriptionState == ComplianceState.ON)
@ -146,7 +148,7 @@ internal class BannerViewModel {
internal enum class ComplianceState {
ON,
STOPPED,
OFF
OFF,
}
internal enum class BannerInfoType {
@ -158,5 +160,5 @@ internal enum class BannerInfoType {
TRANSCRIPTION_STOPPED,
RECORDING_STOPPED_STILL_TRANSCRIBING,
RECORDING_STOPPED,
RECORDING_AND_TRANSCRIPTION_STOPPED
RECORDING_AND_TRANSCRIPTION_STOPPED,
}

Просмотреть файл

@ -95,7 +95,7 @@ internal class ControlBarView : ConstraintLayout {
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.getCallStateFlow().collect() {
viewModel.getCallStateFlow().collect {
if (it == CallingStatus.NONE || it == CallingStatus.CONNECTING) {
cameraToggle.isEnabled = false
micToggle.isEnabled = false
@ -121,18 +121,19 @@ internal class ControlBarView : ConstraintLayout {
private fun accessibilityNonSelectableViews() = setOf(micToggle, cameraToggle)
private val alwaysOffSelectedAccessibilityDelegate = object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
if (host in accessibilityNonSelectableViews()) {
// From an accessibility standpoint these views are never "selected"
info.isSelected = false
private val alwaysOffSelectedAccessibilityDelegate =
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat,
) {
super.onInitializeAccessibilityNodeInfo(host, info)
if (host in accessibilityNonSelectableViews()) {
// From an accessibility standpoint these views are never "selected"
info.isSelected = false
}
}
}
}
private fun setupAccessibility() {
ViewCompat.setAccessibilityDelegate(
@ -141,7 +142,7 @@ internal class ControlBarView : ConstraintLayout {
override fun onRequestSendAccessibilityEvent(
host: ViewGroup,
child: View,
event: AccessibilityEvent
event: AccessibilityEvent,
): Boolean {
if (child in accessibilityNonSelectableViews() && event.eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) {
// We don't want Accessibility TalkBock to read out the "Selected" status of
@ -151,7 +152,7 @@ internal class ControlBarView : ConstraintLayout {
}
return super.onRequestSendAccessibilityEvent(host, child, event)
}
}
},
)
ViewCompat.setAccessibilityDelegate(micToggle, alwaysOffSelectedAccessibilityDelegate)
ViewCompat.setAccessibilityDelegate(cameraToggle, alwaysOffSelectedAccessibilityDelegate)
@ -206,18 +207,18 @@ internal class ControlBarView : ConstraintLayout {
when (audioDeviceSelectionStatus) {
AudioDeviceSelectionStatus.SPEAKER_SELECTED -> {
callAudioDeviceButton.setImageResource(
R.drawable.azure_communication_ui_calling_speaker_speakerphone_selector
R.drawable.azure_communication_ui_calling_speaker_speakerphone_selector,
)
}
AudioDeviceSelectionStatus.RECEIVER_SELECTED -> {
callAudioDeviceButton.setImageResource(
R.drawable.azure_communication_ui_calling_speaker_receiver_selector
R.drawable.azure_communication_ui_calling_speaker_receiver_selector,
)
}
AudioDeviceSelectionStatus.BLUETOOTH_SCO_SELECTED -> {
callAudioDeviceButton.setImageResource(
// Needs an icon
R.drawable.azure_communication_ui_calling_speaker_bluetooth_selector
R.drawable.azure_communication_ui_calling_speaker_bluetooth_selector,
)
}
else -> {}

Просмотреть файл

@ -5,10 +5,10 @@ package com.azure.android.communication.ui.calling.presentation.fragment.calling
import com.azure.android.communication.ui.calling.redux.action.Action
import com.azure.android.communication.ui.calling.redux.action.LocalParticipantAction
import com.azure.android.communication.ui.calling.redux.state.CallingState
import com.azure.android.communication.ui.calling.redux.state.AudioDeviceSelectionStatus
import com.azure.android.communication.ui.calling.redux.state.AudioOperationalStatus
import com.azure.android.communication.ui.calling.redux.state.AudioState
import com.azure.android.communication.ui.calling.redux.state.CallingState
import com.azure.android.communication.ui.calling.redux.state.CallingStatus
import com.azure.android.communication.ui.calling.redux.state.CameraState
import com.azure.android.communication.ui.calling.redux.state.PermissionState

Просмотреть файл

@ -22,9 +22,8 @@ import kotlinx.coroutines.launch
@SuppressLint("ViewConstructor")
internal class MoreCallOptionsListView(
context: Context,
private val viewModel: MoreCallOptionsListViewModel
private val viewModel: MoreCallOptionsListViewModel,
) : RelativeLayout(context) {
private var recyclerView: RecyclerView
private lateinit var menuDrawer: DrawerDialog
private lateinit var bottomCellAdapter: BottomCellAdapter
@ -67,41 +66,44 @@ internal class MoreCallOptionsListView(
recyclerView.layoutManager = LinearLayoutManager(context)
}
private val bottomCellItems get() = viewModel.listEntries.map { entry ->
BottomCellItem(
icon = ContextCompat.getDrawable(
context,
entry.icon ?: android.R.drawable.ic_dialog_alert
),
title = context.getString(entry.title),
contentDescription = null,
accessoryImage = null,
accessoryColor = null,
accessoryImageDescription = null,
isChecked = false,
participantViewData = null,
isOnHold = false,
onClickAction =
{
when (entry) {
MoreCallOptionsListViewModel.Companion.Entries.SHARE_DIAGNOSTICS -> shareDiagnostics(context)
MoreCallOptionsListViewModel.Companion.Entries.REPORT_ISSUE -> viewModel.requestReportIssueScreen()
}
menuDrawer.dismissDialog()
}
)
}
private val bottomCellItems get() =
viewModel.listEntries.map { entry ->
BottomCellItem(
icon =
ContextCompat.getDrawable(
context,
entry.icon ?: android.R.drawable.ic_dialog_alert,
),
title = context.getString(entry.title),
contentDescription = null,
accessoryImage = null,
accessoryColor = null,
accessoryImageDescription = null,
isChecked = false,
participantViewData = null,
isOnHold = false,
onClickAction =
{
when (entry) {
MoreCallOptionsListViewModel.Companion.Entries.SHARE_DIAGNOSTICS -> shareDiagnostics(context)
MoreCallOptionsListViewModel.Companion.Entries.REPORT_ISSUE -> viewModel.requestReportIssueScreen()
}
menuDrawer.dismissDialog()
},
)
}
private fun shareDiagnostics(context: Context) {
val share = Intent.createChooser(
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, viewModel.callId)
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, context.getString(R.string.azure_communication_ui_calling_view_share_diagnostics_title))
},
null
)
val share =
Intent.createChooser(
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, viewModel.callId)
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, context.getString(R.string.azure_communication_ui_calling_view_share_diagnostics_title))
},
null,
)
context.startActivity(share)
}
}

Просмотреть файл

@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
internal class MoreCallOptionsListViewModel(
private val debugInfoManager: DebugInfoManager,
private val showSupportFormOption: Boolean,
private val dispatch: Dispatch
private val dispatch: Dispatch,
) {
private val unknown = "UNKNOWN"
val callId: String
@ -23,12 +23,13 @@ internal class MoreCallOptionsListViewModel(
val displayStateFlow = MutableStateFlow(false)
val listEntries = mutableListOf<Entries>().apply {
add(Entries.SHARE_DIAGNOSTICS)
if (showSupportFormOption) {
add(Entries.REPORT_ISSUE)
val listEntries =
mutableListOf<Entries>().apply {
add(Entries.SHARE_DIAGNOSTICS)
if (showSupportFormOption) {
add(Entries.REPORT_ISSUE)
}
}
}
fun display() {
displayStateFlow.value = true
@ -43,14 +44,14 @@ internal class MoreCallOptionsListViewModel(
}
companion object {
enum class Entries(val title: Int, val icon: Int?,) {
enum class Entries(val title: Int, val icon: Int?) {
SHARE_DIAGNOSTICS(
R.string.azure_communication_ui_calling_view_share_diagnostics,
R.drawable.azure_communication_ui_calling_ic_fluent_share_android_24_regular
R.drawable.azure_communication_ui_calling_ic_fluent_share_android_24_regular,
),
REPORT_ISSUE(
R.string.azure_communication_ui_calling_report_issue_title,
R.drawable.azure_communication_ui_calling_ic_fluent_person_feedback_24_regular
R.drawable.azure_communication_ui_calling_ic_fluent_person_feedback_24_regular,
),
}
}

Просмотреть файл

@ -27,7 +27,6 @@ internal class LeaveConfirmView(
private val viewModel: LeaveConfirmViewModel,
context: Context,
) : RelativeLayout(context) {
private var leaveConfirmMenuTable: RecyclerView
private lateinit var leaveConfirmMenuDrawer: DrawerDialog
private lateinit var bottomCellAdapter: BottomCellAdapter
@ -47,9 +46,7 @@ internal class LeaveConfirmView(
removeAllViews()
}
fun start(
viewLifecycleOwner: LifecycleOwner
) {
fun start(viewLifecycleOwner: LifecycleOwner) {
bottomCellAdapter = BottomCellAdapter()
bottomCellAdapter.setBottomCellItems(bottomCellItems)
leaveConfirmMenuTable.adapter = bottomCellAdapter
@ -85,66 +82,66 @@ internal class LeaveConfirmView(
private val bottomCellItems: List<BottomCellItem>
get() {
val bottomCellItems = mutableListOf(
// Leave title
BottomCellItem(
null,
context.getString(R.string.azure_communication_ui_calling_view_leave_call),
context.getString(R.string.azure_communication_ui_calling_view_leave_confirm_menu),
null,
null,
null,
null,
null,
false,
BottomCellItemType.BottomMenuTitle,
null
),
// Leave
BottomCellItem(
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_leave_confirm_telephone_24
val bottomCellItems =
mutableListOf(
// Leave title
BottomCellItem(
null,
context.getString(R.string.azure_communication_ui_calling_view_leave_call),
context.getString(R.string.azure_communication_ui_calling_view_leave_confirm_menu),
null,
null,
null,
null,
null,
false,
BottomCellItemType.BottomMenuTitle,
null,
),
context.getString(R.string.azure_communication_ui_calling_view_leave_call_button_text),
null,
null,
null,
null,
null,
null,
false,
onClickAction = {
viewModel.confirm()
}
),
// Cancel
BottomCellItem(
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_leave_confirm_dismiss_24
// Leave
BottomCellItem(
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_leave_confirm_telephone_24,
),
context.getString(R.string.azure_communication_ui_calling_view_leave_call_button_text),
null,
null,
null,
null,
null,
null,
false,
onClickAction = {
viewModel.confirm()
},
),
// Cancel
BottomCellItem(
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_leave_confirm_dismiss_24,
),
context.getString(R.string.azure_communication_ui_calling_view_leave_call_cancel),
null,
null,
null,
null,
null,
null,
false,
onClickAction = {
cancelLeaveConfirm()
},
),
context.getString(R.string.azure_communication_ui_calling_view_leave_call_cancel),
null,
null,
null,
null,
null,
null,
false,
onClickAction = {
cancelLeaveConfirm()
},
)
)
return bottomCellItems
}
class AccessibilityManipulatingLinearLayoutManager(context: Context) : LinearLayoutManager(context) {
override fun getRowCountForAccessibility(
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
): Int {
return max(super.getRowCountForAccessibility(recycler, state) - 1, 0)
}
@ -153,12 +150,20 @@ internal class LeaveConfirmView(
recycler: RecyclerView.Recycler,
state: RecyclerView.State,
host: View,
info: AccessibilityNodeInfoCompat
info: AccessibilityNodeInfoCompat,
) {
super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info)
try {
info?.let {
val itemInfo = AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(max(info.collectionItemInfo.rowIndex - 1, 0), info.collectionItemInfo.rowSpan, info.collectionItemInfo.columnIndex, info.collectionItemInfo.columnSpan, info.collectionItemInfo.isHeading, info.collectionItemInfo.isSelected)
val itemInfo =
AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
max(info.collectionItemInfo.rowIndex - 1, 0),
info.collectionItemInfo.rowSpan,
info.collectionItemInfo.columnIndex,
info.collectionItemInfo.columnSpan,
info.collectionItemInfo.isHeading,
info.collectionItemInfo.isSelected,
)
if (info.collectionItemInfo.rowIndex == 0) {
info.setCollectionItemInfo(null)
} else {

Просмотреть файл

@ -57,7 +57,7 @@ internal class InfoHeaderView : ConstraintLayout {
viewLifecycleOwner: LifecycleOwner,
infoHeaderViewModel: InfoHeaderViewModel,
displayParticipantList: () -> Unit,
accessibilityEnabled: Boolean
accessibilityEnabled: Boolean,
) {
this.infoHeaderViewModel = infoHeaderViewModel
this.displayParticipantListCallback = displayParticipantList
@ -78,16 +78,18 @@ internal class InfoHeaderView : ConstraintLayout {
viewLifecycleOwner.lifecycleScope.launch {
infoHeaderViewModel.getNumberOfParticipantsFlow().collect {
participantNumberText.text = when (it) {
0 -> context.getString(R.string.azure_communication_ui_calling_view_info_header_waiting_for_others_to_join)
participantNumberText.text =
when (it) {
0 -> context.getString(R.string.azure_communication_ui_calling_view_info_header_waiting_for_others_to_join)
1 -> context.getString(R.string.azure_communication_ui_calling_view_info_header_call_with_1_person)
1 -> context.getString(R.string.azure_communication_ui_calling_view_info_header_call_with_1_person)
else -> resources.getString(
R.string.azure_communication_ui_calling_view_info_header_call_with_n_people,
it
)
}
else ->
resources.getString(
R.string.azure_communication_ui_calling_view_info_header_call_with_n_people,
it,
)
}
}
}

Просмотреть файл

@ -27,9 +27,7 @@ internal class InfoHeaderViewModel(val multitaskingEnabled: Boolean) {
return numberOfParticipantsFlow
}
fun update(
numberOfRemoteParticipants: Int,
) {
fun update(numberOfRemoteParticipants: Int) {
numberOfParticipantsFlow.value = numberOfRemoteParticipants
if (!displayedOnLaunch) {
displayedOnLaunch = true
@ -68,7 +66,7 @@ internal class InfoHeaderViewModel(val multitaskingEnabled: Boolean) {
displayFloatingHeaderFlow.value = false
}
},
3000
3000,
)
}

Просмотреть файл

@ -47,10 +47,11 @@ internal class OnHoldOverlayView : LinearLayout {
findViewById(R.id.azure_communication_ui_call_hold_overlay_wait_for_host_image)
overlayTitle = findViewById(R.id.azure_communication_ui_call_hold_overlay_title)
resumeButton = findViewById(R.id.azure_communication_ui_call_hold_resume_button)
resumeButton.background = ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_corner_radius_rectangle_4dp_primary_background
)
resumeButton.background =
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_corner_radius_rectangle_4dp_primary_background,
)
}
fun start(
@ -83,7 +84,7 @@ internal class OnHoldOverlayView : LinearLayout {
info.removeAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK)
info.isClickable = false
}
}
},
)
initSnackBar()
@ -124,45 +125,47 @@ internal class OnHoldOverlayView : LinearLayout {
}
private fun initSnackBar() {
snackBar = Snackbar.make(
rootView,
"",
Snackbar.LENGTH_INDEFINITE,
Snackbar.Style.REGULAR
).apply {
animationMode = BaseTransientBottomBar.ANIMATION_MODE_FADE
setAction(rootView.context!!.getText(R.string.azure_communication_ui_calling_snack_bar_button_dismiss)) {
}
if (context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
anchorView =
rootView.findViewById(R.id.azure_communication_ui_call_call_buttons)
}
view.background.colorFilter = PorterDuffColorFilter(
ContextCompat.getColor(
rootView.context,
R.color.azure_communication_ui_calling_color_snack_bar_background
),
PorterDuff.Mode.SRC_IN
)
snackBarTextView = view.findViewById(R.id.snackbar_text)
snackBarTextView.setTextColor(
ContextCompat.getColor(
rootView.context,
R.color.azure_communication_ui_calling_color_snack_bar_text_color
)
)
view.findViewById<AppCompatButton>(R.id.snackbar_action).apply {
setTextColor(
snackBar =
Snackbar.make(
rootView,
"",
Snackbar.LENGTH_INDEFINITE,
Snackbar.Style.REGULAR,
).apply {
animationMode = BaseTransientBottomBar.ANIMATION_MODE_FADE
setAction(rootView.context!!.getText(R.string.azure_communication_ui_calling_snack_bar_button_dismiss)) {
}
if (context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
anchorView =
rootView.findViewById(R.id.azure_communication_ui_call_call_buttons)
}
view.background.colorFilter =
PorterDuffColorFilter(
ContextCompat.getColor(
rootView.context,
R.color.azure_communication_ui_calling_color_snack_bar_background,
),
PorterDuff.Mode.SRC_IN,
)
snackBarTextView = view.findViewById(R.id.snackbar_text)
snackBarTextView.setTextColor(
ContextCompat.getColor(
rootView.context,
R.color.azure_communication_ui_calling_color_snack_bar_text_color
)
R.color.azure_communication_ui_calling_color_snack_bar_text_color,
),
)
isAllCaps = false
contentDescription =
rootView.context.getText(R.string.azure_communication_ui_calling_snack_bar_button_dismiss)
view.findViewById<AppCompatButton>(R.id.snackbar_action).apply {
setTextColor(
ContextCompat.getColor(
rootView.context,
R.color.azure_communication_ui_calling_color_snack_bar_text_color,
),
)
isAllCaps = false
contentDescription =
rootView.context.getText(R.string.azure_communication_ui_calling_snack_bar_button_dismiss)
}
}
}
}
private fun setupUi() {

Просмотреть файл

@ -15,6 +15,7 @@ internal class OnHoldOverlayViewModel(private val dispatch: (Action) -> Unit) {
private lateinit var displayMicUsedToast: MutableStateFlow<Boolean>
fun getDisplayHoldOverlayFlow(): StateFlow<Boolean> = displayHoldOverlayFlow
fun getDisplayMicUsedToastStateFlow(): StateFlow<Boolean> = displayMicUsedToast
fun init(
@ -39,6 +40,5 @@ internal class OnHoldOverlayViewModel(private val dispatch: (Action) -> Unit) {
dispatch(AudioSessionAction.AudioFocusRequesting())
}
private fun shouldDisplayHoldOverlay(callingStatus: CallingStatus) =
callingStatus == CallingStatus.LOCAL_HOLD
private fun shouldDisplayHoldOverlay(callingStatus: CallingStatus) = callingStatus == CallingStatus.LOCAL_HOLD
}

Просмотреть файл

@ -54,12 +54,15 @@ internal class ConnectingLobbyOverlayView : LinearLayout {
ViewCompat.setAccessibilityDelegate(
this,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat,
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.removeAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK)
info.isClickable = false
}
}
},
)
}
}

Просмотреть файл

@ -6,8 +6,8 @@ package com.azure.android.communication.ui.calling.presentation.fragment.calling
import com.azure.android.communication.ui.calling.error.ErrorCode
import com.azure.android.communication.ui.calling.error.FatalError
import com.azure.android.communication.ui.calling.presentation.manager.NetworkManager
import com.azure.android.communication.ui.calling.redux.action.ErrorAction
import com.azure.android.communication.ui.calling.redux.action.Action
import com.azure.android.communication.ui.calling.redux.action.ErrorAction
import com.azure.android.communication.ui.calling.redux.action.PermissionAction
import com.azure.android.communication.ui.calling.redux.state.AudioOperationalStatus
import com.azure.android.communication.ui.calling.redux.state.AudioState
@ -22,7 +22,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
internal class ConnectingLobbyOverlayViewModel(private val dispatch: (Action) -> Unit) {
private lateinit var displayLobbyOverlayFlow: MutableStateFlow<Boolean>
private lateinit var networkManager: NetworkManager
@ -76,12 +75,13 @@ internal class ConnectingLobbyOverlayViewModel(private val dispatch: (Action) ->
fun handleMicrophoneAccessFailed() {
dispatchAction(
action = ErrorAction.FatalErrorOccurred(
FatalError(
Throwable(),
ErrorCode.MICROPHONE_NOT_AVAILABLE
)
)
action =
ErrorAction.FatalErrorOccurred(
FatalError(
Throwable(),
ErrorCode.MICROPHONE_NOT_AVAILABLE,
),
),
)
}
@ -93,20 +93,23 @@ internal class ConnectingLobbyOverlayViewModel(private val dispatch: (Action) ->
dispatch(action)
}
private fun shouldDisplayLobbyOverlay(callingState: CallingState, permissionState: PermissionState) =
((callingState.callingStatus == CallingStatus.NONE) || (callingState.callingStatus == CallingStatus.CONNECTING)) &&
(permissionState.audioPermissionState != PermissionStatus.DENIED) &&
(callingState.operationStatus == OperationStatus.SKIP_SETUP_SCREEN)
private fun shouldDisplayLobbyOverlay(
callingState: CallingState,
permissionState: PermissionState,
) = ((callingState.callingStatus == CallingStatus.NONE) || (callingState.callingStatus == CallingStatus.CONNECTING)) &&
(permissionState.audioPermissionState != PermissionStatus.DENIED) &&
(callingState.operationStatus == OperationStatus.SKIP_SETUP_SCREEN)
private fun handleOffline(networkManager: NetworkManager) {
if (!networkManager.isNetworkConnectionAvailable()) {
dispatchAction(
action = ErrorAction.FatalErrorOccurred(
FatalError(
Throwable(),
ErrorCode.INTERNET_NOT_AVAILABLE
)
)
action =
ErrorAction.FatalErrorOccurred(
FatalError(
Throwable(),
ErrorCode.INTERNET_NOT_AVAILABLE,
),
),
)
}
}
@ -114,12 +117,13 @@ internal class ConnectingLobbyOverlayViewModel(private val dispatch: (Action) ->
private fun handlePermissionDeniedEvent(permissionState: PermissionState) {
if (permissionState.audioPermissionState == PermissionStatus.DENIED) {
dispatchAction(
action = ErrorAction.FatalErrorOccurred(
FatalError(
Throwable(),
ErrorCode.MIC_PERMISSION_DENIED
)
)
action =
ErrorAction.FatalErrorOccurred(
FatalError(
Throwable(),
ErrorCode.MIC_PERMISSION_DENIED,
),
),
)
}
}

Просмотреть файл

@ -41,7 +41,7 @@ internal class LobbyErrorHeaderView : ConstraintLayout {
fun start(
viewLifecycleOwner: LifecycleOwner,
lobbyErrorHeaderViewModel: LobbyErrorHeaderViewModel
lobbyErrorHeaderViewModel: LobbyErrorHeaderViewModel,
) {
this.lobbyErrorHeaderViewModel = lobbyErrorHeaderViewModel
@ -69,10 +69,22 @@ internal class LobbyErrorHeaderView : ConstraintLayout {
private fun getLobbyErrorMessage(it: CallCompositeLobbyErrorCode?): String {
return when (it) {
CallCompositeLobbyErrorCode.LOBBY_DISABLED_BY_CONFIGURATIONS -> context.getString(R.string.azure_communication_ui_calling_error_lobby_disabled_by_configuration)
CallCompositeLobbyErrorCode.LOBBY_CONVERSATION_TYPE_NOT_SUPPORTED -> context.getString(R.string.azure_communication_ui_calling_error_lobby_conversation_type_not_supported)
CallCompositeLobbyErrorCode.LOBBY_MEETING_ROLE_NOT_ALLOWED -> context.getString(R.string.azure_communication_ui_calling_error_lobby_meeting_role_not_allowded)
CallCompositeLobbyErrorCode.REMOVE_PARTICIPANT_OPERATION_FAILURE -> context.getString(R.string.azure_communication_ui_calling_error_lobby_failed_to_remove_participant)
CallCompositeLobbyErrorCode.LOBBY_DISABLED_BY_CONFIGURATIONS ->
context.getString(
R.string.azure_communication_ui_calling_error_lobby_disabled_by_configuration,
)
CallCompositeLobbyErrorCode.LOBBY_CONVERSATION_TYPE_NOT_SUPPORTED ->
context.getString(
R.string.azure_communication_ui_calling_error_lobby_conversation_type_not_supported,
)
CallCompositeLobbyErrorCode.LOBBY_MEETING_ROLE_NOT_ALLOWED ->
context.getString(
R.string.azure_communication_ui_calling_error_lobby_meeting_role_not_allowded,
)
CallCompositeLobbyErrorCode.REMOVE_PARTICIPANT_OPERATION_FAILURE ->
context.getString(
R.string.azure_communication_ui_calling_error_lobby_failed_to_remove_participant,
)
else -> context.getString(R.string.azure_communication_ui_calling_error_lobby_unknown)
}
}

Просмотреть файл

@ -21,7 +21,7 @@ internal class LobbyErrorHeaderViewModel(private val dispatch: (Action) -> Unit)
fun update(
callingStatus: CallingStatus,
error: CallCompositeLobbyErrorCode?,
canShowLobby: Boolean
canShowLobby: Boolean,
) {
displayLobbyErrorHeaderFlow.value = error != null &&
callingStatus == CallingStatus.CONNECTED &&
@ -32,13 +32,14 @@ internal class LobbyErrorHeaderViewModel(private val dispatch: (Action) -> Unit)
fun init(
callingStatus: CallingStatus,
error: CallCompositeLobbyErrorCode?,
canShowLobby: Boolean
canShowLobby: Boolean,
) {
displayLobbyErrorHeaderFlow = MutableStateFlow(
error != null &&
callingStatus == CallingStatus.CONNECTED &&
canShowLobby
)
displayLobbyErrorHeaderFlow =
MutableStateFlow(
error != null &&
callingStatus == CallingStatus.CONNECTED &&
canShowLobby,
)
lobbyErrorFlow = MutableStateFlow(error)
}

Просмотреть файл

@ -45,7 +45,7 @@ internal class LobbyHeaderView : ConstraintLayout {
fun start(
viewLifecycleOwner: LifecycleOwner,
lobbyHeaderViewModel: LobbyHeaderViewModel,
displayParticipantList: () -> Unit
displayParticipantList: () -> Unit,
) {
this.lobbyHeaderViewModel = lobbyHeaderViewModel
this.displayParticipantListCallback = displayParticipantList

Просмотреть файл

@ -31,16 +31,15 @@ internal class LobbyHeaderViewModel {
canShowLobby: Boolean,
) {
var isNewLobbyParticipantAdded = isNewParticipantAdded(lobbyParticipants)
displayLobbyHeaderFlow = MutableStateFlow(
lobbyParticipants.isNotEmpty() &&
(isNewLobbyParticipantAdded || displayLobbyHeaderFlow.value) &&
callingStatus == CallingStatus.CONNECTED && canShowLobby
)
displayLobbyHeaderFlow =
MutableStateFlow(
lobbyParticipants.isNotEmpty() &&
(isNewLobbyParticipantAdded || displayLobbyHeaderFlow.value) &&
callingStatus == CallingStatus.CONNECTED && canShowLobby,
)
}
private fun isNewParticipantAdded(
lobbyParticipants: Map<String, ParticipantInfoModel>
): Boolean {
private fun isNewParticipantAdded(lobbyParticipants: Map<String, ParticipantInfoModel>): Boolean {
var isNewLobbyParticipantAdded = false
if (lobbyParticipantsCache.size < lobbyParticipants.size) {
isNewLobbyParticipantAdded = true

Просмотреть файл

@ -51,12 +51,15 @@ internal class WaitingLobbyOverlayView : LinearLayout {
ViewCompat.setAccessibilityDelegate(
this,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat,
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.removeAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK)
info.isClickable = false
}
}
},
)
}

Просмотреть файл

@ -12,20 +12,15 @@ internal class WaitingLobbyOverlayViewModel {
fun getDisplayLobbyOverlayFlow(): StateFlow<Boolean> = displayLobbyOverlayFlow
fun init(
callingState: CallingStatus,
) {
fun init(callingState: CallingStatus) {
val displayLobbyOverlay = shouldDisplayLobbyOverlay(callingState)
displayLobbyOverlayFlow = MutableStateFlow(displayLobbyOverlay)
}
fun update(
callingState: CallingStatus,
) {
fun update(callingState: CallingStatus) {
val displayLobbyOverlay = shouldDisplayLobbyOverlay(callingState)
displayLobbyOverlayFlow.value = displayLobbyOverlay
}
private fun shouldDisplayLobbyOverlay(callingStatus: CallingStatus) =
callingStatus == CallingStatus.IN_LOBBY
private fun shouldDisplayLobbyOverlay(callingStatus: CallingStatus) = callingStatus == CallingStatus.IN_LOBBY
}

Просмотреть файл

@ -7,7 +7,6 @@ import android.view.MotionEvent
import android.view.View
internal class DragTouchListener internal constructor() : View.OnTouchListener {
private var curX = 0f
private var curY = 0f
private var startX = 0f
@ -25,7 +24,10 @@ internal class DragTouchListener internal constructor() : View.OnTouchListener {
view.translationY = 0F
}
override fun onTouch(view: View, event: MotionEvent): Boolean {
override fun onTouch(
view: View,
event: MotionEvent,
): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startX = event.rawX

Просмотреть файл

@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
internal class LocalParticipantView : ConstraintLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
@ -96,7 +95,6 @@ internal class LocalParticipantView : ConstraintLayout {
videoViewManager: VideoViewManager,
avatarViewManager: AvatarViewManager,
) {
this.viewModel = viewModel
this.videoViewManager = videoViewManager
@ -166,12 +164,13 @@ internal class LocalParticipantView : ConstraintLayout {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.getCameraDeviceSelectionFlow().collect { cameraDeviceSelectionStatus ->
listOf(switchCameraButton, pipSwitchCameraButton).forEach {
it.contentDescription = context.getString(
when (cameraDeviceSelectionStatus) {
CameraDeviceSelectionStatus.FRONT -> R.string.azure_communication_ui_calling_switch_camera_button_front
else -> R.string.azure_communication_ui_calling_switch_camera_button_back
}
)
it.contentDescription =
context.getString(
when (cameraDeviceSelectionStatus) {
CameraDeviceSelectionStatus.FRONT -> R.string.azure_communication_ui_calling_switch_camera_button_front
else -> R.string.azure_communication_ui_calling_switch_camera_button_back
},
)
}
}
}
@ -181,12 +180,12 @@ internal class LocalParticipantView : ConstraintLayout {
if (it) {
ViewCompat.setImportantForAccessibility(
switchCameraButton,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
)
} else {
ViewCompat.setImportantForAccessibility(
switchCameraButton,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES,
)
}
}
@ -225,10 +224,11 @@ internal class LocalParticipantView : ConstraintLayout {
localParticipantPip.visibility =
if (model.viewMode == LocalParticipantViewMode.SELFIE_PIP) View.VISIBLE else View.GONE
val videoHolder = when (model.viewMode) {
LocalParticipantViewMode.SELFIE_PIP -> localParticipantPipCameraHolder
LocalParticipantViewMode.FULL_SCREEN -> localParticipantFullCameraHolder
}
val videoHolder =
when (model.viewMode) {
LocalParticipantViewMode.SELFIE_PIP -> localParticipantPipCameraHolder
LocalParticipantViewMode.FULL_SCREEN -> localParticipantFullCameraHolder
}
if (model.shouldDisplayVideo) {
addVideoView(model.videoStreamID!!, videoHolder, model.viewMode)
@ -238,20 +238,21 @@ internal class LocalParticipantView : ConstraintLayout {
private fun addVideoView(
videoStreamID: String,
videoHolder: ConstraintLayout,
viewMode: LocalParticipantViewMode
viewMode: LocalParticipantViewMode,
) {
val scalingMode = if (isAndroidTV(context)) ScalingMode.FIT else ScalingMode.CROP
videoViewManager.getLocalVideoRenderer(
videoStreamID,
scalingMode
scalingMode,
)?.let { view ->
view.background = this.context.let {
ContextCompat.getDrawable(
it,
R.drawable.azure_communication_ui_calling_corner_radius_rectangle_4dp
)
}
view.background =
this.context.let {
ContextCompat.getDrawable(
it,
R.drawable.azure_communication_ui_calling_corner_radius_rectangle_4dp,
)
}
videoHolder.addView(view, 0)
}
}

Просмотреть файл

@ -34,14 +34,20 @@ internal class LocalParticipantViewModel(
private lateinit var isVisibleFlow: MutableStateFlow<Boolean>
fun getVideoStatusFlow(): StateFlow<VideoModel> = videoStatusFlow
fun getDisplayFullScreenAvatarFlow(): StateFlow<Boolean> = displayFullScreenAvatarFlow
fun getDisplayNameStateFlow(): StateFlow<String?> = displayNameStateFlow
fun getLocalUserMutedStateFlow(): StateFlow<Boolean> = localUserMutedStateFlow
fun getDisplaySwitchCameraButtonFlow(): StateFlow<Boolean> = displaySwitchCameraButtonFlow
fun getDisplayPipSwitchCameraButtonFlow(): StateFlow<Boolean> = displayPipSwitchCameraButtonFlow
fun getEnableCameraSwitchFlow(): StateFlow<Boolean> = enableCameraSwitchFlow
fun getCameraDeviceSelectionFlow(): StateFlow<CameraDeviceSelectionStatus> =
cameraDeviceSelectionFlow
fun getCameraDeviceSelectionFlow(): StateFlow<CameraDeviceSelectionStatus> = cameraDeviceSelectionFlow
fun getIsVisibleFlow(): StateFlow<Boolean> = isVisibleFlow
@ -85,7 +91,12 @@ internal class LocalParticipantViewModel(
isVisibleFlow.value = isVisible(displayVideo, pipStatus, displayFullScreenAvatar, avMode)
}
private fun isVisible(displayVideo: Boolean, pipStatus: PictureInPictureStatus, displayFullScreenAvatar: Boolean, avMode: CallCompositeAudioVideoMode): Boolean {
private fun isVisible(
displayVideo: Boolean,
pipStatus: PictureInPictureStatus,
displayFullScreenAvatar: Boolean,
avMode: CallCompositeAudioVideoMode,
): Boolean {
if (avMode == CallCompositeAudioVideoMode.AUDIO_ONLY && !displayFullScreenAvatar) {
return false
}
@ -112,7 +123,6 @@ internal class LocalParticipantViewModel(
pipStatus: PictureInPictureStatus,
avMode: CallCompositeAudioVideoMode,
) {
val viewMode = getLocalParticipantViewMode(numberOfRemoteParticipants)
val displayVideo = shouldDisplayVideo(videoStreamID)
val displayLobbyOverlay = shouldDisplayLobbyOverlay(callingState)
@ -128,13 +138,14 @@ internal class LocalParticipantViewModel(
MutableStateFlow(
displayVideo &&
viewMode == LocalParticipantViewMode.FULL_SCREEN && camerasCount > 1 &&
pipStatus == PictureInPictureStatus.VISIBLE
pipStatus == PictureInPictureStatus.VISIBLE,
)
displayPipSwitchCameraButtonFlow =
MutableStateFlow(displayVideo && viewMode == LocalParticipantViewMode.SELFIE_PIP && camerasCount > 1)
enableCameraSwitchFlow = MutableStateFlow(
cameraDeviceSelectionStatus != CameraDeviceSelectionStatus.SWITCHING
)
enableCameraSwitchFlow =
MutableStateFlow(
cameraDeviceSelectionStatus != CameraDeviceSelectionStatus.SWITCHING,
)
cameraDeviceSelectionFlow = MutableStateFlow(cameraDeviceSelectionStatus)
isOverlayDisplayedFlow = MutableStateFlow(isOverlayDisplayed(callingState))
numberOfRemoteParticipantsFlow = MutableStateFlow(numberOfRemoteParticipants)
@ -157,15 +168,16 @@ internal class LocalParticipantViewModel(
displayVideo: Boolean,
displayLobbyOverlay: Boolean,
viewMode: LocalParticipantViewMode,
) =
!displayVideo && viewMode == LocalParticipantViewMode.FULL_SCREEN && !displayLobbyOverlay
) = !displayVideo && viewMode == LocalParticipantViewMode.FULL_SCREEN && !displayLobbyOverlay
private fun shouldDisplayLobbyOverlay(callingStatus: CallingStatus) =
callingStatus == CallingStatus.IN_LOBBY
private fun shouldDisplayLobbyOverlay(callingStatus: CallingStatus) = callingStatus == CallingStatus.IN_LOBBY
private fun getLocalParticipantViewMode(numberOfRemoteParticipants: Int): LocalParticipantViewMode {
return if (numberOfRemoteParticipants > 0)
LocalParticipantViewMode.SELFIE_PIP else LocalParticipantViewMode.FULL_SCREEN
return if (numberOfRemoteParticipants > 0) {
LocalParticipantViewMode.SELFIE_PIP
} else {
LocalParticipantViewMode.FULL_SCREEN
}
}
private fun isOverlayDisplayed(callingStatus: CallingStatus) =

Просмотреть файл

@ -41,7 +41,7 @@ internal class ToastNotificationView : ConstraintLayout {
fun start(
viewLifecycleOwner: LifecycleOwner,
toastNotificationViewModel: ToastNotificationViewModel,
accessibilityEnabled: Boolean
accessibilityEnabled: Boolean,
) {
this.toastNotificationViewModel = toastNotificationViewModel
viewLifecycleOwner.lifecycleScope.launch {
@ -62,8 +62,8 @@ internal class ToastNotificationView : ConstraintLayout {
toastNotificationIconImageView.setImageDrawable(
ContextCompat.getDrawable(
context,
it.notificationIconId
)
it.notificationIconId,
),
)
toastNotificationMessageTextView.text = context.getString(it.notificationMessageId)
}

Просмотреть файл

@ -28,8 +28,8 @@ internal class ToastNotificationViewModel(private val dispatch: (Action) -> Unit
0,
0,
null,
null
)
null,
),
)
private var timer: Timer = Timer()
@ -45,16 +45,20 @@ internal class ToastNotificationViewModel(private val dispatch: (Action) -> Unit
if (callDiagnosticsState.networkQualityCallDiagnostic.diagnosticValue == CallDiagnosticQuality.BAD ||
callDiagnosticsState.networkQualityCallDiagnostic.diagnosticValue == CallDiagnosticQuality.POOR
) {
val toastNotificationModel = ToastNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_wifi_warning_24_regular,
R.string.azure_communication_ui_calling_diagnostics_network_quality_low,
if (callDiagnosticsState.networkQualityCallDiagnostic?.diagnosticKind == NetworkCallDiagnostic.NETWORK_RECEIVE_QUALITY)
NetworkCallDiagnostic.NETWORK_RECEIVE_QUALITY else NetworkCallDiagnostic.NETWORK_SEND_QUALITY,
null
)
val toastNotificationModel =
ToastNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_wifi_warning_24_regular,
R.string.azure_communication_ui_calling_diagnostics_network_quality_low,
if (callDiagnosticsState.networkQualityCallDiagnostic?.diagnosticKind == NetworkCallDiagnostic.NETWORK_RECEIVE_QUALITY) {
NetworkCallDiagnostic.NETWORK_RECEIVE_QUALITY
} else {
NetworkCallDiagnostic.NETWORK_SEND_QUALITY
},
null,
)
displayToastNotification(
toastNotificationModel,
false
false,
)
} else {
if (isPersistentNotificationDisplayed) {
@ -68,15 +72,16 @@ internal class ToastNotificationViewModel(private val dispatch: (Action) -> Unit
if (callDiagnosticsState.networkQualityCallDiagnostic.diagnosticValue == CallDiagnosticQuality.BAD ||
callDiagnosticsState.networkQualityCallDiagnostic.diagnosticValue == CallDiagnosticQuality.POOR
) {
val toastNotificationModel = ToastNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_wifi_warning_24_regular,
R.string.azure_communication_ui_calling_diagnostics_network_reconnecting,
NetworkCallDiagnostic.NETWORK_RECONNECTION_QUALITY,
null
)
val toastNotificationModel =
ToastNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_wifi_warning_24_regular,
R.string.azure_communication_ui_calling_diagnostics_network_reconnecting,
NetworkCallDiagnostic.NETWORK_RECONNECTION_QUALITY,
null,
)
displayToastNotification(
toastNotificationModel,
false
false,
)
} else {
if (isPersistentNotificationDisplayed) {
@ -92,13 +97,17 @@ internal class ToastNotificationViewModel(private val dispatch: (Action) -> Unit
when (callDiagnosticsState.networkCallDiagnostic?.diagnosticKind) {
NetworkCallDiagnostic.NETWORK_UNAVAILABLE, NetworkCallDiagnostic.NETWORK_RELAYS_UNREACHABLE -> {
if (callDiagnosticsState.networkCallDiagnostic.diagnosticValue) {
val toastNotificationModel = ToastNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_wifi_warning_24_regular,
R.string.azure_communication_ui_calling_diagnostics_network_was_lost,
if (callDiagnosticsState.networkQualityCallDiagnostic?.diagnosticKind == NetworkCallDiagnostic.NETWORK_UNAVAILABLE)
NetworkCallDiagnostic.NETWORK_UNAVAILABLE else NetworkCallDiagnostic.NETWORK_RELAYS_UNREACHABLE,
null
)
val toastNotificationModel =
ToastNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_wifi_warning_24_regular,
R.string.azure_communication_ui_calling_diagnostics_network_was_lost,
if (callDiagnosticsState.networkQualityCallDiagnostic?.diagnosticKind == NetworkCallDiagnostic.NETWORK_UNAVAILABLE) {
NetworkCallDiagnostic.NETWORK_UNAVAILABLE
} else {
NetworkCallDiagnostic.NETWORK_RELAYS_UNREACHABLE
},
null,
)
displayToastNotification(toastNotificationModel)
}
}
@ -108,12 +117,13 @@ internal class ToastNotificationViewModel(private val dispatch: (Action) -> Unit
when (callDiagnosticsState.mediaCallDiagnostic?.diagnosticKind) {
MediaCallDiagnostic.SPEAKING_WHILE_MICROPHONE_IS_MUTED -> {
if (callDiagnosticsState.mediaCallDiagnostic.diagnosticValue) {
val toastNotificationModel = ToastNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_mic_off_24_filled,
R.string.azure_communication_ui_calling_diagnostics_you_are_muted,
null,
MediaCallDiagnostic.SPEAKING_WHILE_MICROPHONE_IS_MUTED
)
val toastNotificationModel =
ToastNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_mic_off_24_filled,
R.string.azure_communication_ui_calling_diagnostics_you_are_muted,
null,
MediaCallDiagnostic.SPEAKING_WHILE_MICROPHONE_IS_MUTED,
)
displayToastNotification(toastNotificationModel)
} else if (!isPersistentNotificationDisplayed) {
dismiss()
@ -121,13 +131,17 @@ internal class ToastNotificationViewModel(private val dispatch: (Action) -> Unit
}
MediaCallDiagnostic.CAMERA_START_FAILED, MediaCallDiagnostic.CAMERA_START_TIMED_OUT -> {
if (callDiagnosticsState.mediaCallDiagnostic.diagnosticValue) {
val toastNotificationModel = ToastNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_video_off_24_regular,
R.string.azure_communication_ui_calling_diagnostics_unable_to_start_camera,
null,
if (callDiagnosticsState.mediaCallDiagnostic?.diagnosticKind == MediaCallDiagnostic.CAMERA_START_FAILED)
MediaCallDiagnostic.CAMERA_START_FAILED else MediaCallDiagnostic.CAMERA_START_TIMED_OUT,
)
val toastNotificationModel =
ToastNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_video_off_24_regular,
R.string.azure_communication_ui_calling_diagnostics_unable_to_start_camera,
null,
if (callDiagnosticsState.mediaCallDiagnostic?.diagnosticKind == MediaCallDiagnostic.CAMERA_START_FAILED) {
MediaCallDiagnostic.CAMERA_START_FAILED
} else {
MediaCallDiagnostic.CAMERA_START_TIMED_OUT
},
)
displayToastNotification(toastNotificationModel)
}
}
@ -143,7 +157,10 @@ internal class ToastNotificationViewModel(private val dispatch: (Action) -> Unit
}
}
private fun displayToastNotification(toastNotificationModel: ToastNotificationModel, autoDismiss: Boolean = true) {
private fun displayToastNotification(
toastNotificationModel: ToastNotificationModel,
autoDismiss: Boolean = true,
) {
if (!isPersistentNotificationDisplayed) {
toastNotificationModelMessageFlow.value = toastNotificationModel
displayToastNotificationFlow.value = true
@ -157,16 +174,18 @@ internal class ToastNotificationViewModel(private val dispatch: (Action) -> Unit
if (toastNotificationModel.networkCallDiagnostic == NetworkCallDiagnostic.NETWORK_UNAVAILABLE ||
toastNotificationModel.networkCallDiagnostic == NetworkCallDiagnostic.NETWORK_RELAYS_UNREACHABLE
) {
val model = NetworkCallDiagnosticModel(
toastNotificationModel.networkCallDiagnostic,
false
)
val model =
NetworkCallDiagnosticModel(
toastNotificationModel.networkCallDiagnostic,
false,
)
dispatch(CallDiagnosticsAction.NetworkCallDiagnosticsDismissed(model))
} else {
val model = NetworkQualityCallDiagnosticModel(
toastNotificationModel.networkCallDiagnostic,
CallDiagnosticQuality.UNKNOWN
)
val model =
NetworkQualityCallDiagnosticModel(
toastNotificationModel.networkCallDiagnostic,
CallDiagnosticQuality.UNKNOWN,
)
dispatch(CallDiagnosticsAction.NetworkQualityCallDiagnosticsDismissed(model))
}
}
@ -176,7 +195,7 @@ internal class ToastNotificationViewModel(private val dispatch: (Action) -> Unit
}
}
},
4000
4000,
)
}
}

Просмотреть файл

@ -29,23 +29,24 @@ internal class UpperMessageBarNotificationLayoutView : LinearLayout {
fun start(
viewLifecycleOwner: LifecycleOwner,
upperMessageBarNotificationLayoutViewModel: UpperMessageBarNotificationLayoutViewModel,
accessibilityEnabled: Boolean
accessibilityEnabled: Boolean,
) {
this.upperMessageBarNotificationLayoutViewModel = upperMessageBarNotificationLayoutViewModel
upperMessageBarNotificationLayout.visibility = View.VISIBLE
viewLifecycleOwner.lifecycleScope.launch {
upperMessageBarNotificationLayoutViewModel.getNewUpperMessageBarNotificationFlow()?.collect() {
upperMessageBarNotificationLayoutViewModel.getNewUpperMessageBarNotificationFlow()?.collect {
if (!it.upperMessageBarNotificationModel.isEmpty()) {
val upperMessageBarNotificationView: UpperMessageBarNotificationView = inflate(
context,
R.layout.azure_communication_ui_calling_upper_message_bar_notification,
null
) as UpperMessageBarNotificationView
val upperMessageBarNotificationView: UpperMessageBarNotificationView =
inflate(
context,
R.layout.azure_communication_ui_calling_upper_message_bar_notification,
null,
) as UpperMessageBarNotificationView
upperMessageBarNotificationView.start(
viewLifecycleOwner,
it,
accessibilityEnabled
accessibilityEnabled,
)
val layoutParams = LinearLayout.LayoutParams(upperMessageBarNotificationLayout.layoutParams)

Просмотреть файл

@ -12,33 +12,34 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
internal class UpperMessageBarNotificationLayoutViewModel(private val dispatch: (Action) -> Unit) {
private var newUpperMessageBarNotificationFlow: MutableStateFlow<UpperMessageBarNotificationViewModel> = MutableStateFlow(
UpperMessageBarNotificationViewModel(
dispatch,
UpperMessageBarNotificationModel(
0,
0,
null
)
private var newUpperMessageBarNotificationFlow: MutableStateFlow<UpperMessageBarNotificationViewModel> =
MutableStateFlow(
UpperMessageBarNotificationViewModel(
dispatch,
UpperMessageBarNotificationModel(
0,
0,
null,
),
),
)
)
private var mediaDiagnosticNotificationViewModels = hashMapOf<MediaCallDiagnostic, UpperMessageBarNotificationViewModel>()
fun getNewUpperMessageBarNotificationFlow(): StateFlow<UpperMessageBarNotificationViewModel> = newUpperMessageBarNotificationFlow
fun update(callDiagnosticsState: CallDiagnosticsState) {
when (callDiagnosticsState.mediaCallDiagnostic?.diagnosticKind) {
MediaCallDiagnostic.NO_SPEAKER_DEVICES_AVAILABLE -> {
if (mediaDiagnosticNotificationViewModels[MediaCallDiagnostic.NO_SPEAKER_DEVICES_AVAILABLE] == null &&
callDiagnosticsState.mediaCallDiagnostic.diagnosticValue
) {
var upperMessageBarNotificationModel = UpperMessageBarNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_speaker_mute_24_regular,
R.string.azure_communication_ui_calling_diagnostics_unable_to_locate_speaker,
MediaCallDiagnostic.NO_SPEAKER_DEVICES_AVAILABLE,
)
var upperMessageBarNotificationModel =
UpperMessageBarNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_speaker_mute_24_regular,
R.string.azure_communication_ui_calling_diagnostics_unable_to_locate_speaker,
MediaCallDiagnostic.NO_SPEAKER_DEVICES_AVAILABLE,
)
addNewNotification(upperMessageBarNotificationModel)
} else if (mediaDiagnosticNotificationViewModels[MediaCallDiagnostic.NO_SPEAKER_DEVICES_AVAILABLE] != null &&
!callDiagnosticsState.mediaCallDiagnostic.diagnosticValue
@ -50,11 +51,12 @@ internal class UpperMessageBarNotificationLayoutViewModel(private val dispatch:
if (mediaDiagnosticNotificationViewModels[MediaCallDiagnostic.NO_MICROPHONE_DEVICES_AVAILABLE] == null &&
callDiagnosticsState.mediaCallDiagnostic.diagnosticValue
) {
var upperMessageBarNotificationModel = UpperMessageBarNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_mic_prohibited_24_regular,
R.string.azure_communication_ui_calling_diagnostics_unable_to_locate_microphone,
MediaCallDiagnostic.NO_MICROPHONE_DEVICES_AVAILABLE
)
var upperMessageBarNotificationModel =
UpperMessageBarNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_mic_prohibited_24_regular,
R.string.azure_communication_ui_calling_diagnostics_unable_to_locate_microphone,
MediaCallDiagnostic.NO_MICROPHONE_DEVICES_AVAILABLE,
)
addNewNotification(upperMessageBarNotificationModel)
} else if (mediaDiagnosticNotificationViewModels[MediaCallDiagnostic.NO_MICROPHONE_DEVICES_AVAILABLE] != null &&
!callDiagnosticsState.mediaCallDiagnostic.diagnosticValue
@ -66,11 +68,12 @@ internal class UpperMessageBarNotificationLayoutViewModel(private val dispatch:
if (mediaDiagnosticNotificationViewModels[MediaCallDiagnostic.MICROPHONE_NOT_FUNCTIONING] == null &&
callDiagnosticsState.mediaCallDiagnostic.diagnosticValue
) {
var upperMessageBarNotificationModel = UpperMessageBarNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_mic_prohibited_24_regular,
R.string.azure_communication_ui_calling_diagnostics_microphone_not_working_as_expected,
MediaCallDiagnostic.MICROPHONE_NOT_FUNCTIONING
)
var upperMessageBarNotificationModel =
UpperMessageBarNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_mic_prohibited_24_regular,
R.string.azure_communication_ui_calling_diagnostics_microphone_not_working_as_expected,
MediaCallDiagnostic.MICROPHONE_NOT_FUNCTIONING,
)
addNewNotification(upperMessageBarNotificationModel)
} else if (mediaDiagnosticNotificationViewModels[MediaCallDiagnostic.MICROPHONE_NOT_FUNCTIONING] != null &&
!callDiagnosticsState.mediaCallDiagnostic.diagnosticValue
@ -82,11 +85,12 @@ internal class UpperMessageBarNotificationLayoutViewModel(private val dispatch:
if (mediaDiagnosticNotificationViewModels[MediaCallDiagnostic.SPEAKER_NOT_FUNCTIONING] == null &&
callDiagnosticsState.mediaCallDiagnostic.diagnosticValue
) {
var upperMessageBarNotificationModel = UpperMessageBarNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_speaker_mute_24_regular,
R.string.azure_communication_ui_calling_diagnostics_speaker_not_working_as_expected,
MediaCallDiagnostic.SPEAKER_NOT_FUNCTIONING
)
var upperMessageBarNotificationModel =
UpperMessageBarNotificationModel(
R.drawable.azure_communication_ui_calling_ic_fluent_speaker_mute_24_regular,
R.string.azure_communication_ui_calling_diagnostics_speaker_not_working_as_expected,
MediaCallDiagnostic.SPEAKER_NOT_FUNCTIONING,
)
addNewNotification(upperMessageBarNotificationModel)
} else if (mediaDiagnosticNotificationViewModels[MediaCallDiagnostic.SPEAKER_NOT_FUNCTIONING] != null &&
!callDiagnosticsState.mediaCallDiagnostic.diagnosticValue
@ -100,10 +104,11 @@ internal class UpperMessageBarNotificationLayoutViewModel(private val dispatch:
private fun addNewNotification(upperMessageBarNotificationModel: UpperMessageBarNotificationModel) {
upperMessageBarNotificationModel.mediaCallDiagnostic?.let {
val upperMessageNotificationViewModel = UpperMessageBarNotificationViewModel(
dispatch,
upperMessageBarNotificationModel
)
val upperMessageNotificationViewModel =
UpperMessageBarNotificationViewModel(
dispatch,
upperMessageBarNotificationModel,
)
mediaDiagnosticNotificationViewModels[upperMessageBarNotificationModel.mediaCallDiagnostic] =
upperMessageNotificationViewModel
newUpperMessageBarNotificationFlow.value = upperMessageNotificationViewModel

Просмотреть файл

@ -47,7 +47,7 @@ internal class UpperMessageBarNotificationView : ConstraintLayout {
fun start(
viewLifecycleOwner: LifecycleOwner,
upperMessageBarNotificationViewModel: UpperMessageBarNotificationViewModel,
accessibilityEnabled: Boolean
accessibilityEnabled: Boolean,
) {
this.upperMessageBarNotificationViewModel = upperMessageBarNotificationViewModel
setupAccessibility()
@ -59,8 +59,8 @@ internal class UpperMessageBarNotificationView : ConstraintLayout {
upperMessageBarNotificationIconImageView.setImageDrawable(
ContextCompat.getDrawable(
context,
it.notificationIconId
)
it.notificationIconId,
),
)
upperMessageBarNotificationMessage.text = context.getString(it.notificationMessageId)
}
@ -68,7 +68,7 @@ internal class UpperMessageBarNotificationView : ConstraintLayout {
}
viewLifecycleOwner.lifecycleScope.launch {
upperMessageBarNotificationViewModel.getDismissUpperMessageBarNotificationFlow()?.collect() {
upperMessageBarNotificationViewModel.getDismissUpperMessageBarNotificationFlow()?.collect {
if (it) {
val viewGroup = upperMessageBarNotificationLayout.parent as ViewGroup
viewGroup.removeView(upperMessageBarNotificationLayout)

Просмотреть файл

@ -12,9 +12,10 @@ import kotlinx.coroutines.flow.StateFlow
internal class UpperMessageBarNotificationViewModel(
private val dispatch: (Action) -> Unit,
val upperMessageBarNotificationModel: UpperMessageBarNotificationModel
val upperMessageBarNotificationModel: UpperMessageBarNotificationModel,
) {
private var upperMessageBarNotificationModelFlow: MutableStateFlow<UpperMessageBarNotificationModel> = MutableStateFlow(upperMessageBarNotificationModel)
private var upperMessageBarNotificationModelFlow: MutableStateFlow<UpperMessageBarNotificationModel> =
MutableStateFlow(upperMessageBarNotificationModel)
private var dismissUpperMessageBarNotificationFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
fun getDismissUpperMessageBarNotificationFlow(): StateFlow<Boolean> = dismissUpperMessageBarNotificationFlow

Просмотреть файл

@ -29,7 +29,6 @@ internal class ParticipantGridCellView(
private val getScreenShareVideoStreamRendererCallback: () -> VideoStreamRenderer?,
private val getParticipantViewDataCallback: (participantID: String) -> CallCompositeParticipantViewData?,
) : RelativeLayout(context) {
private lateinit var avatarView: ParticipantGridCellAvatarView
private lateinit var videoView: ParticipantGridCellVideoView
@ -70,18 +69,19 @@ internal class ParticipantGridCellView(
val onHoldTextView: TextView =
findViewById(R.id.azure_communication_ui_calling_participant_audio_view_on_hold)
avatarView = ParticipantGridCellAvatarView(
avatarControl,
participantAvatarSpeakingIndicator,
participantAvatarContainer,
displayNameAudioTextView,
micIndicatorAudioImageView,
getParticipantViewDataCallback,
participantViewModel,
onHoldTextView,
context,
lifecycleScope,
)
avatarView =
ParticipantGridCellAvatarView(
avatarControl,
participantAvatarSpeakingIndicator,
participantAvatarContainer,
displayNameAudioTextView,
micIndicatorAudioImageView,
getParticipantViewDataCallback,
participantViewModel,
onHoldTextView,
context,
lifecycleScope,
)
}
private fun createVideoView() {
@ -100,19 +100,20 @@ internal class ParticipantGridCellView(
val micIndicatorOnVideoImageView: ImageView =
findViewById(R.id.azure_communication_ui_participant_view_on_video_mic_indicator)
videoView = ParticipantGridCellVideoView(
context,
lifecycleScope,
participantVideoContainerFrameLayout,
videoContainer,
displayNameAndMicIndicatorViewContainer,
displayNameOnVideoTextView,
micIndicatorOnVideoImageView,
participantViewModel,
getVideoStreamCallback,
showFloatingHeaderCallBack,
getScreenShareVideoStreamRendererCallback,
getParticipantViewDataCallback,
)
videoView =
ParticipantGridCellVideoView(
context,
lifecycleScope,
participantVideoContainerFrameLayout,
videoContainer,
displayNameAndMicIndicatorViewContainer,
displayNameOnVideoTextView,
micIndicatorOnVideoImageView,
participantViewModel,
getVideoStreamCallback,
showFloatingHeaderCallBack,
getScreenShareVideoStreamRendererCallback,
getParticipantViewDataCallback,
)
}
}

Просмотреть файл

@ -25,14 +25,15 @@ internal class ParticipantGridCellViewModel(
private var isMutedStateFlow = MutableStateFlow(isMuted)
private var isSpeakingStateFlow = MutableStateFlow(isSpeaking && !isMuted)
private var isNameIndicatorVisibleStateFlow = MutableStateFlow(true)
private var videoViewModelStateFlow = MutableStateFlow(
getVideoStreamModel(
createVideoViewModel(cameraVideoStreamModel),
createVideoViewModel(screenShareVideoStreamModel),
isOnHoldStateFlow.value,
isCameraDisabled,
private var videoViewModelStateFlow =
MutableStateFlow(
getVideoStreamModel(
createVideoViewModel(cameraVideoStreamModel),
createVideoViewModel(screenShareVideoStreamModel),
isOnHoldStateFlow.value,
isCameraDisabled,
),
)
)
private var participantModifiedTimestamp = modifiedTimestamp
private var participantUserIdentifier = userIdentifier
@ -69,9 +70,7 @@ internal class ParticipantGridCellViewModel(
return isOnHoldStateFlow
}
fun update(
participant: ParticipantInfoModel,
) {
fun update(participant: ParticipantInfoModel) {
this.participantUserIdentifier = participant.userIdentifier
this.displayNameStateFlow.value = participant.displayName
this.isMutedStateFlow.value = participant.isMuted
@ -80,12 +79,13 @@ internal class ParticipantGridCellViewModel(
this.isNameIndicatorVisibleStateFlow.value =
!(participant.displayName.isBlank() && !participant.isMuted)
this.videoViewModelStateFlow.value = getVideoStreamModel(
createVideoViewModel(participant.cameraVideoStreamModel),
createVideoViewModel(participant.screenShareVideoStreamModel),
this.isOnHoldStateFlow.value,
participant.isCameraDisabled
)
this.videoViewModelStateFlow.value =
getVideoStreamModel(
createVideoViewModel(participant.cameraVideoStreamModel),
createVideoViewModel(participant.screenShareVideoStreamModel),
this.isOnHoldStateFlow.value,
participant.isCameraDisabled,
)
this.isSpeakingStateFlow.value = participant.isSpeaking && !participant.isMuted
this.participantModifiedTimestamp = participant.modifiedTimestamp
@ -102,7 +102,7 @@ internal class ParticipantGridCellViewModel(
cameraVideoStreamModel: VideoViewModel?,
screenShareVideoStreamModel: VideoViewModel?,
isOnHold: Boolean,
isCameraDisabled: Boolean
isCameraDisabled: Boolean,
): VideoViewModel? {
if (isOnHold) return null
if (screenShareVideoStreamModel != null) return screenShareVideoStreamModel
@ -111,6 +111,5 @@ internal class ParticipantGridCellViewModel(
return null
}
private fun isOnHold(participantStatus: ParticipantStatus?) =
participantStatus == ParticipantStatus.HOLD
private fun isOnHold(participantStatus: ParticipantStatus?) = participantStatus == ParticipantStatus.HOLD
}

Просмотреть файл

@ -51,7 +51,12 @@ internal class ParticipantGridView : GridLayout {
private lateinit var displayedRemoteParticipantsView: MutableList<ParticipantGridCellView>
private lateinit var getParticipantViewDataCallback: (participantID: String) -> CallCompositeParticipantViewData?
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
override fun onSizeChanged(
w: Int,
h: Int,
oldw: Int,
oldh: Int,
) {
super.onSizeChanged(w, h, oldw, oldh)
post {
updateGrid(participantGridViewModel.getRemoteParticipantsUpdateStateFlow().value)
@ -85,7 +90,7 @@ internal class ParticipantGridView : GridLayout {
info.removeAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK)
info.isClickable = false
}
}
},
)
}
@ -96,7 +101,7 @@ internal class ParticipantGridView : GridLayout {
this.getVideoStreamCallback = { participantID: String, videoStreamID: String ->
this.videoViewManager.getRemoteVideoStreamRenderer(
participantID,
videoStreamID
videoStreamID,
)
}
@ -139,40 +144,51 @@ internal class ParticipantGridView : GridLayout {
if (it) {
ViewCompat.setImportantForAccessibility(
gridView,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
)
} else {
ViewCompat.setImportantForAccessibility(
gridView,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES,
)
}
}
}
addOnLayoutChangeListener(object : OnLayoutChangeListener {
override fun onLayoutChange(
v: View,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int,
) {
if (isLaidOut) {
removeOnLayoutChangeListener(this)
post {
updateGrid(participantGridViewModel.getRemoteParticipantsUpdateStateFlow().value)
addOnLayoutChangeListener(
object : OnLayoutChangeListener {
override fun onLayoutChange(
v: View,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int,
) {
if (isLaidOut) {
removeOnLayoutChangeListener(this)
post {
updateGrid(participantGridViewModel.getRemoteParticipantsUpdateStateFlow().value)
}
}
}
}
})
},
)
addOnLayoutChangeListener { _, left, top, right, bottom,
oldLeft, oldTop, oldRight, oldBottom ->
addOnLayoutChangeListener {
_,
left,
top,
right,
bottom,
oldLeft,
oldTop,
oldRight,
oldBottom,
->
if (left != oldLeft ||
right != oldRight ||
top != oldTop ||
@ -190,9 +206,7 @@ internal class ParticipantGridView : GridLayout {
removeAllViews()
}
private fun updateGrid(
displayedRemoteParticipantsViewModel: List<ParticipantGridCellViewModel>,
) {
private fun updateGrid(displayedRemoteParticipantsViewModel: List<ParticipantGridCellViewModel>) {
videoViewManager.updateScalingForRemoteStream()
removeAllViews()
displayedRemoteParticipantsView = mutableListOf()
@ -206,14 +220,12 @@ internal class ParticipantGridView : GridLayout {
displayParticipants(displayedRemoteParticipantsView)
}
private fun displayParticipants(
displayedRemoteParticipantsView: List<ParticipantGridCellView>,
) {
private fun displayParticipants(displayedRemoteParticipantsView: List<ParticipantGridCellView>) {
when (displayedRemoteParticipantsView.size) {
SINGLE_PARTICIPANT, TWO_PARTICIPANTS, FOUR_PARTICIPANTS, SIX_PARTICIPANTS, NINE_PARTICIPANTS, -> {
SINGLE_PARTICIPANT, TWO_PARTICIPANTS, FOUR_PARTICIPANTS, SIX_PARTICIPANTS, NINE_PARTICIPANTS -> {
displayedRemoteParticipantsView.forEach {
addParticipantToGrid(
participantGridCellView = it
participantGridCellView = it,
)
}
}
@ -222,19 +234,19 @@ internal class ParticipantGridView : GridLayout {
if (context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
addParticipantToGrid(
columnSpan = 2,
participantGridCellView = displayedRemoteParticipantsView[0]
participantGridCellView = displayedRemoteParticipantsView[0],
)
} else {
addParticipantToGrid(
rowSpan = 2,
participantGridCellView = displayedRemoteParticipantsView[0]
participantGridCellView = displayedRemoteParticipantsView[0],
)
}
displayedRemoteParticipantsView.forEachIndexed { index, participantGridCellView ->
if (index > 0) {
addParticipantToGrid(
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
}
}
@ -246,7 +258,7 @@ internal class ParticipantGridView : GridLayout {
addParticipantToGrid(
columnSpan = 2,
rowSpan = 3,
participantGridCellView = displayedRemoteParticipantsView[0]
participantGridCellView = displayedRemoteParticipantsView[0],
)
displayedRemoteParticipantsView.forEachIndexed { index, participantGridCellView ->
@ -255,13 +267,13 @@ internal class ParticipantGridView : GridLayout {
addParticipantToGrid(
columnSpan = 2,
rowSpan = 3,
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
} else {
addParticipantToGrid(
columnSpan = 2,
rowSpan = 2,
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
}
}
@ -272,13 +284,13 @@ internal class ParticipantGridView : GridLayout {
addParticipantToGrid(
rowSpan = 2,
columnSpan = 2,
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
} else {
addParticipantToGrid(
columnSpan = 3,
rowSpan = 2,
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
}
}
@ -287,19 +299,19 @@ internal class ParticipantGridView : GridLayout {
if (context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
addParticipantToGrid(
columnSpan = 2,
participantGridCellView = displayedRemoteParticipantsView[0]
participantGridCellView = displayedRemoteParticipantsView[0],
)
} else {
addParticipantToGrid(
rowSpan = 2,
participantGridCellView = displayedRemoteParticipantsView[0]
participantGridCellView = displayedRemoteParticipantsView[0],
)
}
displayedRemoteParticipantsView.forEachIndexed { index, participantGridCellView ->
if (index > 0) {
addParticipantToGrid(
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
}
}
@ -309,23 +321,23 @@ internal class ParticipantGridView : GridLayout {
if (context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
addParticipantToGrid(
rowSpan = 4,
participantGridCellView = displayedRemoteParticipantsView[0]
participantGridCellView = displayedRemoteParticipantsView[0],
)
addParticipantToGrid(
rowSpan = 3,
participantGridCellView = displayedRemoteParticipantsView[1]
participantGridCellView = displayedRemoteParticipantsView[1],
)
displayedRemoteParticipantsView.forEachIndexed { index, participantGridCellView ->
if (index > 1) {
if (index % 2 == 0) {
addParticipantToGrid(
rowSpan = 3,
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
} else {
addParticipantToGrid(
rowSpan = 4,
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
}
}
@ -335,12 +347,12 @@ internal class ParticipantGridView : GridLayout {
if (index < 4) {
addParticipantToGrid(
columnSpan = 3,
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
} else {
addParticipantToGrid(
columnSpan = 4,
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
}
}
@ -353,13 +365,13 @@ internal class ParticipantGridView : GridLayout {
addParticipantToGrid(
rowSpan = 2,
columnSpan = 3,
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
} else {
addParticipantToGrid(
rowSpan = 2,
columnSpan = 2,
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
}
}
@ -367,7 +379,7 @@ internal class ParticipantGridView : GridLayout {
addParticipantToGrid(
rowSpan = 3,
columnSpan = 2,
participantGridCellView = displayedRemoteParticipantsView[0]
participantGridCellView = displayedRemoteParticipantsView[0],
)
displayedRemoteParticipantsView.forEachIndexed { index, participantGridCellView ->
@ -376,13 +388,13 @@ internal class ParticipantGridView : GridLayout {
addParticipantToGrid(
rowSpan = 3,
columnSpan = 2,
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
} else {
addParticipantToGrid(
rowSpan = 2,
columnSpan = 2,
participantGridCellView = participantGridCellView
participantGridCellView = participantGridCellView,
)
}
}
@ -461,7 +473,10 @@ internal class ParticipantGridView : GridLayout {
this.addView(participantGridCellView)
}
private fun setGridRowsColumn(rows: Int, columns: Int) {
private fun setGridRowsColumn(
rows: Int,
columns: Int,
) {
this.rowCount = rows
this.columnCount = columns
}
@ -487,6 +502,6 @@ internal class ParticipantGridView : GridLayout {
showFloatingHeaderCallBack,
getVideoStreamCallback,
getScreenShareVideoStreamRendererCallback,
getParticipantViewDataCallback
getParticipantViewDataCallback,
)
}

Просмотреть файл

@ -13,9 +13,8 @@ import java.lang.Integer.min
internal class ParticipantGridViewModel(
private val participantGridCellViewModelFactory: ParticipantGridCellViewModelFactory,
private val maxRemoteParticipantSize: Int
private val maxRemoteParticipantSize: Int,
) {
private var remoteParticipantsUpdatedStateFlow: MutableStateFlow<List<ParticipantGridCellViewModel>> =
MutableStateFlow(mutableListOf())
@ -28,9 +27,7 @@ internal class ParticipantGridViewModel(
private var pipStatus: PictureInPictureStatus = PictureInPictureStatus.VISIBLE
private lateinit var isLobbyOverlayDisplayedFlow: MutableStateFlow<Boolean>
fun init(
callingStatus: CallingStatus,
) {
fun init(callingStatus: CallingStatus) {
isLobbyOverlayDisplayedFlow = MutableStateFlow(isLobbyOverlayDisplayed(callingStatus))
}
@ -50,8 +47,11 @@ internal class ParticipantGridViewModel(
}
fun getMaxRemoteParticipantsSize(): Int {
return if (pipStatus == PictureInPictureStatus.VISIBLE)
maxRemoteParticipantSize else 1
return if (pipStatus == PictureInPictureStatus.VISIBLE) {
maxRemoteParticipantSize
} else {
1
}
}
fun getIsLobbyOverlayDisplayedFlow(): StateFlow<Boolean> = isLobbyOverlayDisplayedFlow
@ -87,12 +87,13 @@ internal class ParticipantGridViewModel(
sortRemoteParticipants(remoteParticipantsMap, dominantSpeakersInfo)
}
} else {
remoteParticipantsMapSorted = mapOf(
Pair(
participantSharingScreen,
remoteParticipantsMap[participantSharingScreen]!!
remoteParticipantsMapSorted =
mapOf(
Pair(
participantSharingScreen,
remoteParticipantsMap[participantSharingScreen]!!,
),
)
)
}
updateRemoteParticipantsVideoStreams(remoteParticipantsMapSorted)
@ -100,9 +101,7 @@ internal class ParticipantGridViewModel(
updateDisplayedParticipants(remoteParticipantsMapSorted.toMutableMap())
}
private fun getParticipantSharingScreen(
remoteParticipantsMap: Map<String, ParticipantInfoModel>,
): String? {
private fun getParticipantSharingScreen(remoteParticipantsMap: Map<String, ParticipantInfoModel>): String? {
remoteParticipantsMap.forEach { (id, participantInfoModel) ->
if (participantInfoModel.screenShareVideoStreamModel != null) {
return id
@ -111,17 +110,16 @@ internal class ParticipantGridViewModel(
return null
}
private fun updateDisplayedParticipants(
remoteParticipantsMapSorted: MutableMap<String, ParticipantInfoModel>,
) {
private fun updateDisplayedParticipants(remoteParticipantsMapSorted: MutableMap<String, ParticipantInfoModel>) {
val alreadyDisplayedParticipants =
displayedRemoteParticipantsViewModelMap.filter { (id, _) ->
remoteParticipantsMapSorted.containsKey(id)
}
val viewModelsThatCanBeRemoved = displayedRemoteParticipantsViewModelMap.keys.filter {
!remoteParticipantsMapSorted.containsKey(it)
}.toMutableList()
val viewModelsThatCanBeRemoved =
displayedRemoteParticipantsViewModelMap.keys.filter {
!remoteParticipantsMapSorted.containsKey(it)
}.toMutableList()
alreadyDisplayedParticipants.forEach { (id, participantViewModel) ->
if (participantViewModel.getParticipantModifiedTimestamp()
@ -179,75 +177,79 @@ internal class ParticipantGridViewModel(
remoteParticipantsMap: Map<String, ParticipantInfoModel>,
dominantSpeakersInfo: List<String>,
): Map<String, ParticipantInfoModel> {
val dominantSpeakersOrder = mutableMapOf<String, Int>()
for (i in 0 until min(maxRemoteParticipantSize, dominantSpeakersInfo.count())) {
dominantSpeakersOrder[dominantSpeakersInfo[i]] = i
}
val lengthComparator = Comparator<Pair<String, ParticipantInfoModel>> { keyValuePair1, keyValuePair2 ->
val participantId1 = keyValuePair1.first
val participantId2 = keyValuePair2.first
val participant1 = keyValuePair1.second
val participant2 = keyValuePair2.second
val lengthComparator =
Comparator<Pair<String, ParticipantInfoModel>> { keyValuePair1, keyValuePair2 ->
val participantId1 = keyValuePair1.first
val participantId2 = keyValuePair2.first
val participant1 = keyValuePair1.second
val participant2 = keyValuePair2.second
if (dominantSpeakersOrder.containsKey(participantId1) &&
dominantSpeakersOrder.containsKey(participantId2)
) {
val order1 = dominantSpeakersOrder.getValue(participantId1)
val order2 = dominantSpeakersOrder.getValue(participantId2)
return@Comparator if (order1 > order2)
1 else -1
if (dominantSpeakersOrder.containsKey(participantId1) &&
dominantSpeakersOrder.containsKey(participantId2)
) {
val order1 = dominantSpeakersOrder.getValue(participantId1)
val order2 = dominantSpeakersOrder.getValue(participantId2)
return@Comparator if (order1 > order2) {
1
} else {
-1
}
}
if (dominantSpeakersOrder.containsKey(participantId1)) {
return@Comparator -1
}
if (dominantSpeakersOrder.containsKey(participantId2)) {
return@Comparator 1
}
if ((participant1.cameraVideoStreamModel != null && participant2.cameraVideoStreamModel != null) ||
(participant1.cameraVideoStreamModel == null && participant2.cameraVideoStreamModel == null)
) {
return@Comparator 0
}
if (participant1.cameraVideoStreamModel != null) {
return@Comparator -1
} else {
return@Comparator 1
}
}
if (dominantSpeakersOrder.containsKey(participantId1))
return@Comparator -1
if (dominantSpeakersOrder.containsKey(participantId2))
return@Comparator 1
if ((participant1.cameraVideoStreamModel != null && participant2.cameraVideoStreamModel != null) ||
(participant1.cameraVideoStreamModel == null && participant2.cameraVideoStreamModel == null)
)
return@Comparator 0
if (participant1.cameraVideoStreamModel != null)
return@Comparator -1
else
return@Comparator 1
}
return remoteParticipantsMap.toList()
.sortedWith(lengthComparator)
.take(getMaxRemoteParticipantsSize()).toMap()
}
private fun updateRemoteParticipantsVideoStreams(
participantViewModelMap: Map<String, ParticipantInfoModel>,
) {
private fun updateRemoteParticipantsVideoStreams(participantViewModelMap: Map<String, ParticipantInfoModel>) {
val usersVideoStream: MutableList<Pair<String, String>> = mutableListOf()
participantViewModelMap.forEach { (participantId, participant) ->
participant.cameraVideoStreamModel?.let {
usersVideoStream.add(
Pair(
participantId,
participant.cameraVideoStreamModel!!.videoStreamID
)
participant.cameraVideoStreamModel!!.videoStreamID,
),
)
}
participant.screenShareVideoStreamModel?.let {
usersVideoStream.add(
Pair(
participantId,
participant.screenShareVideoStreamModel!!.videoStreamID
)
participant.screenShareVideoStreamModel!!.videoStreamID,
),
)
}
}
updateVideoStreamsCallback?.invoke(usersVideoStream)
}
private fun isLobbyOverlayDisplayed(callingStatus: CallingStatus) =
callingStatus == CallingStatus.IN_LOBBY
private fun isLobbyOverlayDisplayed(callingStatus: CallingStatus) = callingStatus == CallingStatus.IN_LOBBY
}

Просмотреть файл

@ -5,8 +5,8 @@ package com.azure.android.communication.ui.calling.presentation.fragment.calling
import android.content.Context
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
@ -53,11 +53,15 @@ internal class ParticipantGridCellAvatarView(
if (it) {
onHoldTextView.visibility = VISIBLE
micIndicatorAudioImageView.visibility = GONE
displayNameAudioTextView.setTextColor(ContextCompat.getColor(context, R.color.azure_communication_ui_calling_color_participant_list_mute_mic))
displayNameAudioTextView.setTextColor(
ContextCompat.getColor(context, R.color.azure_communication_ui_calling_color_participant_list_mute_mic),
)
} else {
onHoldTextView.visibility = INVISIBLE
setMicButtonVisibility(participantViewModel.getIsMutedStateFlow().value)
displayNameAudioTextView.setTextColor(ContextCompat.getColor(context, R.color.azure_communication_ui_calling_color_on_background))
displayNameAudioTextView.setTextColor(
ContextCompat.getColor(context, R.color.azure_communication_ui_calling_color_on_background),
)
}
}
}
@ -100,14 +104,13 @@ internal class ParticipantGridCellAvatarView(
}
}
private fun setSpeakingIndicator(
isSpeaking: Boolean,
) {
private fun setSpeakingIndicator(isSpeaking: Boolean) {
if (isSpeaking) {
participantAvatarSpeakingFrameLayout.background = ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_speaking_round_indicator
)
participantAvatarSpeakingFrameLayout.background =
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_speaking_round_indicator,
)
} else {
participantAvatarSpeakingFrameLayout.setBackgroundResource(0)
}

Просмотреть файл

@ -5,9 +5,9 @@ package com.azure.android.communication.ui.calling.presentation.fragment.calling
import android.content.Context
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.View.GONE
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
@ -16,8 +16,8 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleCoroutineScope
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.ui.calling.models.StreamType
import com.azure.android.communication.ui.calling.models.CallCompositeParticipantViewData
import com.azure.android.communication.ui.calling.models.StreamType
import com.azure.android.communication.ui.calling.presentation.fragment.calling.participant.grid.ParticipantGridCellViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.participant.grid.VideoViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.participant.grid.screenshare.ScreenShareViewManager
@ -94,9 +94,7 @@ internal class ParticipantGridCellVideoView(
}
}
private fun updateVideoStream(
videoViewModel: VideoViewModel?,
) {
private fun updateVideoStream(videoViewModel: VideoViewModel?) {
if (videoStream != null) {
detachFromParentView(videoStream)
videoStream = null
@ -105,7 +103,7 @@ internal class ParticipantGridCellVideoView(
if (videoViewModel != null) {
getVideoStreamCallback(
participantViewModel.getParticipantUserIdentifier(),
videoViewModel.videoStreamID
videoViewModel.videoStreamID,
)?.let { view ->
videoStream = view
setRendererView(view, videoViewModel.streamType)
@ -125,9 +123,7 @@ internal class ParticipantGridCellVideoView(
}
}
private fun setSpeakingIndicator(
isSpeaking: Boolean,
) {
private fun setSpeakingIndicator(isSpeaking: Boolean) {
if (isSpeaking) {
participantVideoContainerSpeakingFrameLayout.visibility = VISIBLE
} else {
@ -135,44 +131,53 @@ internal class ParticipantGridCellVideoView(
}
}
private fun setRendererView(rendererView: View, streamType: StreamType) {
private fun setRendererView(
rendererView: View,
streamType: StreamType,
) {
detachFromParentView(rendererView)
if (streamType == StreamType.SCREEN_SHARING) {
removeScreenShareZoomView()
val screenShareFactory = ScreenShareViewManager(
context,
videoContainer,
getScreenShareVideoStreamRendererCallback,
showFloatingHeaderCallBack
)
val screenShareFactory =
ScreenShareViewManager(
context,
videoContainer,
getScreenShareVideoStreamRendererCallback,
showFloatingHeaderCallBack,
)
screenShareZoomFrameLayout = screenShareFactory.getScreenShareView(rendererView)
videoContainer.addView(screenShareZoomFrameLayout, 0)
// scaled transformed view round corners are not visible when scroll is not at end
// to avoid content outside speaking rectangle removing round corners
videoContainer.background = ContextCompat.getDrawable(
context,
R.color.azure_communication_ui_calling_color_surface
)
participantVideoContainerSpeakingFrameLayout.background = ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_speaking_rectangle_indicator_no_corner
)
videoContainer.background =
ContextCompat.getDrawable(
context,
R.color.azure_communication_ui_calling_color_surface,
)
participantVideoContainerSpeakingFrameLayout.background =
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_speaking_rectangle_indicator_no_corner,
)
return
}
rendererView.background = ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_corner_radius_rectangle_4dp
)
videoContainer.background = ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_corner_radius_rectangle_4dp_surface
)
participantVideoContainerSpeakingFrameLayout.background = ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_speaking_rectangle_indicator
)
rendererView.background =
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_corner_radius_rectangle_4dp,
)
videoContainer.background =
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_corner_radius_rectangle_4dp_surface,
)
participantVideoContainerSpeakingFrameLayout.background =
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_speaking_rectangle_indicator,
)
videoContainer.addView(rendererView, 0)
}

Просмотреть файл

@ -19,7 +19,6 @@ internal class GestureListener(
) : ScaleGestureDetector.OnScaleGestureListener,
GestureDetector.OnGestureListener,
GestureDetector.OnDoubleTapListener {
companion object {
private const val INVALID_POINTER = MotionEvent.INVALID_POINTER_ID
}

Просмотреть файл

@ -7,7 +7,10 @@ import android.view.MotionEvent
internal interface GestureListenerEvents {
fun onSingleClick()
fun onDoubleClick(motionEvent: MotionEvent)
fun initTransformation()
fun updateTransformation()
}

Просмотреть файл

@ -37,13 +37,14 @@ internal class ScreenShareViewManager(
screenShareZoomFrameLayout.layoutParams =
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
FrameLayout.LayoutParams.MATCH_PARENT,
)
screenShareZoomFrameLayout.addView(rendererViewTransformationWrapper)
screenShareZoomFrameLayout.setFloatingHeaderCallback(showFloatingHeaderCallBack)
screenShareZoomFrameLayout.viewTreeObserver.addOnGlobalLayoutListener(object :
screenShareZoomFrameLayout.viewTreeObserver.addOnGlobalLayoutListener(
object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
screenShareZoomFrameLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
@ -53,7 +54,8 @@ internal class ScreenShareViewManager(
setScreenShareLayoutSize()
}, STREAM_SIZE_RETRY_DURATION)
}
})
},
)
return screenShareZoomFrameLayout
}

Просмотреть файл

@ -102,8 +102,11 @@ internal class ScreenShareZoomFrameLayout :
}
override fun onLayout(
changed: Boolean, left: Int,
top: Int, right: Int, bottom: Int,
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int,
) {
super.onLayout(changed, left, top, right, bottom)
getScreenShareViewBounds(screenShareViewBounds)
@ -182,20 +185,22 @@ internal class ScreenShareZoomFrameLayout :
val rect = RectF()
rect.set(screenShareViewBounds)
transform.mapRect(rect)
val offsetLeft = getOffset(
rect.left,
rect.right,
zoomFrameViewBounds.left,
zoomFrameViewBounds.right,
screenShareViewBounds.centerX()
)
val offsetTop = getOffset(
rect.top,
rect.bottom,
zoomFrameViewBounds.top,
zoomFrameViewBounds.bottom,
screenShareViewBounds.centerY()
)
val offsetLeft =
getOffset(
rect.left,
rect.right,
zoomFrameViewBounds.left,
zoomFrameViewBounds.right,
screenShareViewBounds.centerX(),
)
val offsetTop =
getOffset(
rect.top,
rect.bottom,
zoomFrameViewBounds.top,
zoomFrameViewBounds.bottom,
screenShareViewBounds.centerY(),
)
if (offsetLeft != 0f || offsetTop != 0f) {
transform.postTranslate(offsetLeft, offsetTop)
@ -232,7 +237,9 @@ internal class ScreenShareZoomFrameLayout :
}
return if (viewEnd < limitEnd) {
limitEnd - viewEnd
} else 0f
} else {
0f
}
}
private fun onDoubleTapDisplay(motionEvent: MotionEvent): Boolean {
@ -257,11 +264,15 @@ internal class ScreenShareZoomFrameLayout :
} else {
if (shouldZoomToMax()) {
zoomToPoint(
MAX_SCALE, screenSharePoint, zoomLayoutPoint
MAX_SCALE,
screenSharePoint,
zoomLayoutPoint,
)
} else {
zoomToPoint(
MIN_SCALE, screenSharePoint, zoomLayoutPoint
MIN_SCALE,
screenSharePoint,
zoomLayoutPoint,
)
}
}
@ -284,10 +295,11 @@ internal class ScreenShareZoomFrameLayout :
}
private fun shouldStartDoubleTapScroll(viewPoint: PointF): Boolean {
val dist = hypot(
(viewPoint.x - doubleTapZoomLayoutPoint.x).toDouble(),
(viewPoint.y - doubleTapZoomLayoutPoint.y).toDouble()
)
val dist =
hypot(
(viewPoint.x - doubleTapZoomLayoutPoint.x).toDouble(),
(viewPoint.y - doubleTapZoomLayoutPoint.y).toDouble(),
)
return dist > 20
}
@ -332,7 +344,11 @@ internal class ScreenShareZoomFrameLayout :
}
}
private fun zoomToPoint(scale: Float, imagePoint: PointF, viewPoint: PointF) {
private fun zoomToPoint(
scale: Float,
imagePoint: PointF,
viewPoint: PointF,
) {
val matrix = Matrix()
applyZoomToPointTransform(matrix, scale, imagePoint, viewPoint)
setTransformAnimated(matrix)
@ -380,20 +396,22 @@ internal class ScreenShareZoomFrameLayout :
activeTransform.set(animatedMatrix)
onTransformChanged()
}
doubleTapZoomAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationCancel(animation: Animator) {
onAnimationStopped()
}
doubleTapZoomAnimator.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationCancel(animation: Animator) {
onAnimationStopped()
}
override fun onAnimationEnd(animation: Animator) {
onAnimationStopped()
}
override fun onAnimationEnd(animation: Animator) {
onAnimationStopped()
}
private fun onAnimationStopped() {
gestureListener.resetPointers()
gestureListener.doubleTapZoomAnimationEnded()
}
})
private fun onAnimationStopped() {
gestureListener.resetPointers()
gestureListener.doubleTapZoomAnimationEnded()
}
},
)
gestureListener.doubleTapZoomAnimationStarted()
doubleTapZoomAnimator.start()
}

Просмотреть файл

@ -113,9 +113,7 @@ internal class ParticipantListView(
participantTable.layoutManager = LinearLayoutManager(context)
}
private fun updateRemoteParticipantListContent(
participantListCellModelList: List<ParticipantListCellModel>,
) {
private fun updateRemoteParticipantListContent(participantListCellModelList: List<ParticipantListCellModel>) {
if (this::bottomCellAdapter.isInitialized) {
val bottomCellItems = generateBottomCellItems(participantListCellModelList)
updateRemoteParticipantListContent(bottomCellItems.size)
@ -128,10 +126,10 @@ internal class ParticipantListView(
private fun updateLocalParticipantCellContent() {
if (this::bottomCellAdapter.isInitialized) {
val bottomCellItems = generateBottomCellItems(
viewModel.getRemoteParticipantListCellStateFlow().value
)
val bottomCellItems =
generateBottomCellItems(
viewModel.getRemoteParticipantListCellStateFlow().value,
)
with(bottomCellAdapter) {
setBottomCellItems(bottomCellItems)
@ -141,7 +139,6 @@ internal class ParticipantListView(
}
private fun updateRemoteParticipantListContent(listSize: Int) {
// title for in call participants
var titles = 1
@ -153,20 +150,19 @@ internal class ParticipantListView(
// set the height of the list to be half of the screen height or 50dp per item, whichever is smaller
participantTable.layoutParams.height =
(((listSize - titles) * 50 * context.resources.displayMetrics.density + titles * 30 * context.resources.displayMetrics.density).toInt()).coerceAtMost(
context.resources.displayMetrics.heightPixels / 2
context.resources.displayMetrics.heightPixels / 2,
)
}
private fun generateBottomCellItems(
remoteParticipantCellModels: List<ParticipantListCellModel>,
): MutableList<BottomCellItem> {
private fun generateBottomCellItems(remoteParticipantCellModels: List<ParticipantListCellModel>): MutableList<BottomCellItem> {
val bottomCellItemsInCallParticipants = mutableListOf<BottomCellItem>()
val bottomCellItemsInLobbyParticipants = mutableListOf<BottomCellItem>()
// since we can not get resources from model class, we create the local participant list cell
// with suffix in this way
val localParticipant = viewModel.createLocalParticipantListCell(
resources.getString(R.string.azure_communication_ui_calling_view_participant_drawer_local_participant)
)
val localParticipant =
viewModel.createLocalParticipantListCell(
resources.getString(R.string.azure_communication_ui_calling_view_participant_drawer_local_participant),
)
val localParticipantViewData =
avatarViewManager.callCompositeLocalOptions?.participantViewData
bottomCellItemsInCallParticipants
@ -174,14 +170,14 @@ internal class ParticipantListView(
generateBottomCellItem(
getLocalParticipantNameToDisplay(
localParticipantViewData,
localParticipant.displayName
localParticipant.displayName,
),
localParticipant.isMuted,
localParticipantViewData,
localParticipant.isOnHold,
localParticipant.userIdentifier,
localParticipant.status
)
localParticipant.status,
),
)
for (remoteParticipant in remoteParticipantCellModels) {
@ -198,8 +194,8 @@ internal class ParticipantListView(
remoteParticipantViewData,
null,
remoteParticipant.userIdentifier,
remoteParticipant.status
)
remoteParticipant.status,
),
)
} else if (remoteParticipant.status != ParticipantStatus.DISCONNECTED) {
bottomCellItemsInCallParticipants.add(
@ -209,8 +205,8 @@ internal class ParticipantListView(
remoteParticipantViewData,
remoteParticipant.isOnHold,
remoteParticipant.userIdentifier,
remoteParticipant.status
)
remoteParticipant.status,
),
)
}
}
@ -221,7 +217,7 @@ internal class ParticipantListView(
null,
context.getString(
R.string.azure_communication_ui_calling_participant_list_in_call_n_people,
bottomCellItemsInCallParticipants.size
bottomCellItemsInCallParticipants.size,
),
"",
null,
@ -231,8 +227,8 @@ internal class ParticipantListView(
null,
false,
BottomCellItemType.BottomMenuTitle,
null
)
null,
),
)
bottomCellItemsInLobbyParticipants.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title!! })
@ -243,7 +239,7 @@ internal class ParticipantListView(
null,
context.getString(
R.string.azure_communication_ui_calling_participant_list_in_lobby_n_people,
bottomCellItemsInLobbyParticipants.size
bottomCellItemsInLobbyParticipants.size,
),
"",
null,
@ -257,8 +253,8 @@ internal class ParticipantListView(
true,
admitAllButtonAction = {
admitAllLobbyParticipants()
}
)
},
),
)
}
return (bottomCellItemsInLobbyParticipants + bottomCellItemsInCallParticipants).toMutableList()
@ -288,27 +284,44 @@ internal class ParticipantListView(
userIdentifier: String,
status: ParticipantStatus?,
): BottomCellItem {
val micIcon = ContextCompat.getDrawable(
context,
if (isMuted == true) R.drawable.azure_communication_ui_calling_ic_fluent_mic_off_24_filled_composite_button_filled_grey
else R.drawable.azure_communication_ui_calling_ic_fluent_mic_on_24_filled_composite_button_filled_grey
)
val micIcon =
ContextCompat.getDrawable(
context,
if (isMuted == true) {
R.drawable.azure_communication_ui_calling_ic_fluent_mic_off_24_filled_composite_button_filled_grey
} else {
R.drawable.azure_communication_ui_calling_ic_fluent_mic_on_24_filled_composite_button_filled_grey
},
)
val micAccessibilityAnnouncement = context.getString(
if (isMuted == true) R.string.azure_communication_ui_calling_view_participant_list_muted_accessibility_label
else R.string.azure_communication_ui_calling_view_participant_list_unmuted_accessibility_label
)
val micAccessibilityAnnouncement =
context.getString(
if (isMuted == true) {
R.string.azure_communication_ui_calling_view_participant_list_muted_accessibility_label
} else {
R.string.azure_communication_ui_calling_view_participant_list_unmuted_accessibility_label
},
)
val onHoldAnnouncement: String = if (isOnHold == true) context.getString(R.string.azure_communication_ui_calling_remote_participant_on_hold) else ""
val onHoldAnnouncement: String =
if (isOnHold == true) {
context.getString(
R.string.azure_communication_ui_calling_remote_participant_on_hold,
)
} else {
""
}
return BottomCellItem(
null,
displayName,
displayName +
if (status == ParticipantStatus.IN_LOBBY)
if (status == ParticipantStatus.IN_LOBBY) {
context.getString(R.string.azure_communication_ui_calling_view_participant_list_dismiss_lobby_list)
else context.getString(R.string.azure_communication_ui_calling_view_participant_list_dismiss_list) +
onHoldAnnouncement,
} else {
context.getString(R.string.azure_communication_ui_calling_view_participant_list_dismiss_list) +
onHoldAnnouncement
},
if (status != ParticipantStatus.IN_LOBBY) micIcon else null,
if (status != ParticipantStatus.IN_LOBBY) R.color.azure_communication_ui_calling_color_participant_list_mute_mic else null,
micAccessibilityAnnouncement,
@ -321,30 +334,33 @@ internal class ParticipantListView(
} else if (accessibilityManager.isEnabled) {
participantListDrawer.dismiss()
}
}
},
)
}
private fun showAdmitDialog(displayName: String?, userIdentifier: String) {
private fun showAdmitDialog(
displayName: String?,
userIdentifier: String,
) {
val builder =
AlertDialog.Builder(context, R.style.AzureCommunicationUICalling_AlertDialog_Theme)
builder.setMessage(
context.getString(
R.string.azure_communication_ui_calling_admit_name,
displayName
)
displayName,
),
)
.setPositiveButton(
context.getString(
R.string.azure_communication_ui_calling_admit
)
R.string.azure_communication_ui_calling_admit,
),
) { _, _ ->
viewModel.admitParticipant(userIdentifier)
}
.setNegativeButton(
context.getString(
R.string.azure_communication_ui_calling_decline
)
R.string.azure_communication_ui_calling_decline,
),
) { _, _ ->
viewModel.declineParticipant(userIdentifier)
}

Просмотреть файл

@ -29,14 +29,19 @@ internal class ParticipantListViewModel(private val dispatch: (Action) -> Unit)
return displayParticipantListStateFlow
}
fun createLocalParticipantListCell(suffix: String) = ParticipantListCellModel(
(localParticipantListCellStateFlow.value.displayName.trim() + " " + suffix).trim(),
localParticipantListCellStateFlow.value.isMuted,
"",
false
)
fun createLocalParticipantListCell(suffix: String) =
ParticipantListCellModel(
(localParticipantListCellStateFlow.value.displayName.trim() + " " + suffix).trim(),
localParticipantListCellStateFlow.value.isMuted,
"",
false,
)
fun init(participantMap: Map<String, ParticipantInfoModel>, localUserState: LocalUserState, canShowLobby: Boolean) {
fun init(
participantMap: Map<String, ParticipantInfoModel>,
localUserState: LocalUserState,
canShowLobby: Boolean,
) {
val remoteParticipantList: List<ParticipantListCellModel> =
participantMap.values.map {
getRemoteParticipantListCellModel(it)
@ -47,7 +52,11 @@ internal class ParticipantListViewModel(private val dispatch: (Action) -> Unit)
MutableStateFlow(getLocalParticipantListCellModel(localUserState))
}
fun update(participantMap: Map<String, ParticipantInfoModel>, localUserState: LocalUserState, canShowLobby: Boolean) {
fun update(
participantMap: Map<String, ParticipantInfoModel>,
localUserState: LocalUserState,
canShowLobby: Boolean,
) {
val remoteParticipantList: MutableList<ParticipantListCellModel> =
participantMap.values.map {
getRemoteParticipantListCellModel(it)
@ -63,7 +72,7 @@ internal class ParticipantListViewModel(private val dispatch: (Action) -> Unit)
) = (
it.status != ParticipantStatus.DISCONNECTED &&
if (it.status == ParticipantStatus.IN_LOBBY) canShowLobby else true
)
)
fun displayParticipantList() {
displayParticipantListStateFlow.value = true
@ -91,13 +100,14 @@ internal class ParticipantListViewModel(private val dispatch: (Action) -> Unit)
localUserDisplayName ?: "",
localUserState.audioState.operation == AudioOperationalStatus.OFF,
"",
false
false,
)
}
private fun getRemoteParticipantListCellModel(it: ParticipantInfoModel): ParticipantListCellModel {
return ParticipantListCellModel(
it.displayName.trim(), it.isMuted,
it.displayName.trim(),
it.isMuted,
it.userIdentifier,
it.participantStatus == ParticipantStatus.HOLD,
it.participantStatus,

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT License.
@file:OptIn(InternalCoroutinesApi::class)
package com.azure.android.communication.ui.calling.presentation.fragment.calling.support
import android.content.Context
@ -27,7 +28,6 @@ import kotlinx.coroutines.launch
* It is displayed when the user clicks on the support button.
*/
internal class SupportView : ConstraintLayout {
private val sendButton: Button by lazy { findViewById(R.id.azure_communication_ui_send_button) }
private val cancelButton: Button by lazy { findViewById(R.id.azure_communication_ui_cancel_button) }
private val editText: EditText by lazy { findViewById(R.id.azure_communication_ui_user_message_edit_text) }
@ -53,7 +53,10 @@ internal class SupportView : ConstraintLayout {
inflate(context, R.layout.azure_communication_ui_calling_support_view, this)
}
fun start(viewModel: SupportViewModel, viewLifecycleOwner: LifecycleOwner) {
fun start(
viewModel: SupportViewModel,
viewLifecycleOwner: LifecycleOwner,
) {
// Text Changed, Submit, Cancel Buttons
bindViewInputs(viewModel)
@ -88,7 +91,7 @@ internal class SupportView : ConstraintLayout {
private fun bindViewOutputs(
viewLifecycleOwner: LifecycleOwner,
viewModel: SupportViewModel
viewModel: SupportViewModel,
) {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.isVisibleStateFlow.collect {

Просмотреть файл

@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
internal class SupportViewModel(private val dispatch: Dispatch, private val onSubmit: (String) -> Unit) {
private var _isVisibleStateFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _isSubmitEnabledStateFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)

Просмотреть файл

@ -24,7 +24,6 @@ internal class AudioDeviceListView(
private val viewModel: AudioDeviceListViewModel,
context: Context,
) : RelativeLayout(context) {
private var deviceTable: RecyclerView
private lateinit var audioDeviceDrawer: DrawerDialog
private lateinit var bottomCellAdapter: BottomCellAdapter
@ -97,7 +96,7 @@ internal class AudioDeviceListView(
BottomCellItem(
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_ic_fluent_speaker_2_24_regular_composite_button_filled
R.drawable.azure_communication_ui_calling_ic_fluent_speaker_2_24_regular_composite_button_filled,
),
when (viewModel.audioStateFlow.value.isHeadphonePlugged) {
true -> context.getString(R.string.azure_communication_ui_calling_audio_device_drawer_headphone)
@ -106,7 +105,7 @@ internal class AudioDeviceListView(
null,
ContextCompat.getDrawable(
context,
R.drawable.ms_ic_checkmark_24_filled
R.drawable.ms_ic_checkmark_24_filled,
),
null,
context.getString(R.string.azure_communication_ui_calling_setup_view_audio_device_selected_accessibility_label),
@ -116,8 +115,8 @@ internal class AudioDeviceListView(
onClickAction = {
viewModel.switchAudioDevice(AudioDeviceSelectionStatus.RECEIVER_REQUESTED)
audioDeviceDrawer.dismiss()
}
)
},
),
)
}
@ -125,13 +124,13 @@ internal class AudioDeviceListView(
BottomCellItem(
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_ic_fluent_speaker_2_24_filled_composite_button_enabled
R.drawable.azure_communication_ui_calling_ic_fluent_speaker_2_24_filled_composite_button_enabled,
),
context.getString(R.string.azure_communication_ui_calling_audio_device_drawer_speaker),
null,
ContextCompat.getDrawable(
context,
R.drawable.ms_ic_checkmark_24_filled
R.drawable.ms_ic_checkmark_24_filled,
),
null,
context.getString(R.string.azure_communication_ui_calling_setup_view_audio_device_selected_accessibility_label),
@ -141,8 +140,8 @@ internal class AudioDeviceListView(
onClickAction = {
viewModel.switchAudioDevice(AudioDeviceSelectionStatus.SPEAKER_REQUESTED)
audioDeviceDrawer.dismiss()
}
)
},
),
)
if (viewModel.audioStateFlow.value.bluetoothState.available) {
@ -152,15 +151,14 @@ internal class AudioDeviceListView(
BottomCellItem(
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_ic_fluent_speaker_bluetooth_24_regular
R.drawable.azure_communication_ui_calling_ic_fluent_speaker_bluetooth_24_regular,
),
viewModel.audioStateFlow.value.bluetoothState.deviceName,
null,
ContextCompat.getDrawable(
context,
R.drawable.ms_ic_checkmark_24_filled
R.drawable.ms_ic_checkmark_24_filled,
),
null,
context.getString(R.string.azure_communication_ui_calling_setup_view_audio_device_selected_accessibility_label),
isChecked = initialDevice == AudioDeviceSelectionStatus.BLUETOOTH_SCO_SELECTED,
@ -169,8 +167,8 @@ internal class AudioDeviceListView(
onClickAction = {
viewModel.switchAudioDevice(AudioDeviceSelectionStatus.BLUETOOTH_SCO_REQUESTED)
audioDeviceDrawer.dismiss()
}
)
},
),
)
}
return bottomCellItems
@ -182,8 +180,8 @@ internal class AudioDeviceListView(
announceForAccessibility(
context.getString(
R.string.azure_communication_ui_calling_selected_audio_device_announcement,
getDeviceTypeName(audioState)
)
getDeviceTypeName(audioState),
),
)
}
}

Просмотреть файл

@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
internal class AudioDeviceListViewModel(private val dispatch: (Action) -> Unit) {
private val displayAudioDeviceSelectionMenuMutableStateFlow = MutableStateFlow(false)
private lateinit var audioStateMutableStateFlow: MutableStateFlow<AudioState>
@ -29,8 +28,8 @@ internal class AudioDeviceListViewModel(private val dispatch: (Action) -> Unit)
fun switchAudioDevice(audioDeviceSelectionStatus: AudioDeviceSelectionStatus) {
dispatch(
LocalParticipantAction.AudioDeviceChangeRequested(
audioDeviceSelectionStatus
)
audioDeviceSelectionStatus,
),
)
}

Просмотреть файл

@ -5,20 +5,20 @@ package com.azure.android.communication.ui.calling.presentation.fragment.factori
import com.azure.android.communication.ui.calling.presentation.fragment.calling.banner.BannerViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.controlbar.ControlBarViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.controlbar.more.MoreCallOptionsListViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.hangup.LeaveConfirmViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.header.InfoHeaderViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.hold.OnHoldOverlayViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.lobby.ConnectingLobbyOverlayViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.lobby.LobbyErrorHeaderViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.lobby.LobbyHeaderViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.lobby.WaitingLobbyOverlayViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.localuser.LocalParticipantViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.notification.ToastNotificationViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.notification.UpperMessageBarNotificationLayoutViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.participant.grid.ParticipantGridViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.participantlist.ParticipantListViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.common.audiodevicelist.AudioDeviceListViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.controlbar.more.MoreCallOptionsListViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.lobby.ConnectingLobbyOverlayViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.notification.ToastNotificationViewModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.notification.UpperMessageBarNotificationLayoutViewModel
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManager
import com.azure.android.communication.ui.calling.redux.Store
import com.azure.android.communication.ui.calling.redux.state.ReduxState
@ -31,12 +31,11 @@ internal class CallingViewModelFactory(
private val showSupportFormOption: Boolean = false,
private val enableMultitasking: Boolean,
) : BaseViewModelFactory(store) {
val moreCallOptionsListViewModel by lazy {
MoreCallOptionsListViewModel(
debugInfoManager,
showSupportFormOption = showSupportFormOption,
store::dispatch
store::dispatch,
)
}

Просмотреть файл

@ -7,9 +7,7 @@ import com.azure.android.communication.ui.calling.models.ParticipantInfoModel
import com.azure.android.communication.ui.calling.presentation.fragment.calling.participant.grid.ParticipantGridCellViewModel
internal class ParticipantGridCellViewModelFactory {
fun ParticipantGridCellViewModel(
participantInfoModel: ParticipantInfoModel,
): ParticipantGridCellViewModel =
fun ParticipantGridCellViewModel(participantInfoModel: ParticipantInfoModel): ParticipantGridCellViewModel =
ParticipantGridCellViewModel(
participantInfoModel.userIdentifier,
participantInfoModel.displayName,

Просмотреть файл

@ -18,7 +18,6 @@ internal class SetupViewModelFactory(
private val store: Store<ReduxState>,
private val context: Context,
) : BaseViewModelFactory(store) {
val audioDeviceListViewModel by lazy {
AudioDeviceListViewModel(store::dispatch)
}
@ -42,7 +41,7 @@ internal class SetupViewModelFactory(
val joinCallButtonHolderViewModel by lazy {
JoinCallButtonHolderViewModel(
store::dispatch,
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager,
)
}
}

Просмотреть файл

@ -31,7 +31,6 @@ import com.azure.android.communication.ui.calling.presentation.fragment.setup.co
internal class SetupFragment :
Fragment(R.layout.azure_communication_ui_calling_fragment_setup) {
// Get the DI Container, which gives us what we need for this fragment (dependencies)
private val holder: DependencyInjectionContainerHolder by activityViewModels()
@ -49,7 +48,10 @@ internal class SetupFragment :
private val networkManager get() = holder.container.networkManager
private val viewModel get() = holder.setupViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
viewModel.init(viewLifecycleOwner.lifecycleScope)
@ -62,7 +64,7 @@ internal class SetupFragment :
view.findViewById(R.id.azure_communication_ui_setup_join_call_holder)
setupJoinCallButtonHolderView.start(
viewLifecycleOwner,
viewModel.joinCallButtonHolderViewModel
viewModel.joinCallButtonHolderViewModel,
)
participantAvatarView = view.findViewById(R.id.azure_communication_ui_setup_default_avatar)
@ -130,26 +132,30 @@ internal class SetupFragment :
get() = (activity as AppCompatActivity)
private fun setActionBarTitle() {
fun setActionbarTextColor(text: SpannableString, @ColorInt color: Int) {
fun setActionbarTextColor(
text: SpannableString,
@ColorInt color: Int,
) {
text.setSpan(
ForegroundColorSpan(
ContextCompat.getColor(
requireContext(),
color
)
color,
),
),
0,
text.length,
Spannable.SPAN_INCLUSIVE_INCLUSIVE
Spannable.SPAN_INCLUSIVE_INCLUSIVE,
)
}
val localOptions = holder.container.configuration.callCompositeLocalOptions
val titleSpan = if (!TextUtils.isEmpty(localOptions?.setupScreenViewData?.title)) {
SpannableString(localOptions?.setupScreenViewData?.title)
} else {
SpannableString(getString(R.string.azure_communication_ui_calling_call_setup_action_bar_title))
}
val titleSpan =
if (!TextUtils.isEmpty(localOptions?.setupScreenViewData?.title)) {
SpannableString(localOptions?.setupScreenViewData?.title)
} else {
SpannableString(getString(R.string.azure_communication_ui_calling_call_setup_action_bar_title))
}
setActionbarTextColor(titleSpan, R.color.azure_communication_ui_calling_color_action_bar_text)
@ -163,7 +169,7 @@ internal class SetupFragment :
callCompositeActivity.supportActionBar?.subtitle = subtitleSpan
} else {
holder.container.logger.error(
"Provided setupScreenViewData has subtitle, but no title provided. In this case subtitle is not displayed."
"Provided setupScreenViewData has subtitle, but no title provided. In this case subtitle is not displayed.",
)
}
}

Просмотреть файл

@ -20,7 +20,6 @@ internal class SetupViewModel(
private val networkManager: NetworkManager,
) :
BaseViewModel(store) {
val warningsViewModel = setupViewModelProvider.warningsViewModel
val setupControlBarViewModel = setupViewModelProvider.setupControlBarViewModel
val localParticipantRendererViewModel = setupViewModelProvider.previewAreaViewModel
@ -68,7 +67,7 @@ internal class SetupViewModel(
)
setupGradientViewModel.init(
state.localParticipantState.videoStreamID,
state.localParticipantState.cameraState.operation
state.localParticipantState.cameraState.operation,
)
participantAvatarViewModel.init(
state.localParticipantState.displayName,
@ -80,14 +79,13 @@ internal class SetupViewModel(
state.permissionState.cameraPermissionState,
state.localParticipantState.cameraState.operation,
state.localParticipantState.cameraState.camerasCount,
networkManager
networkManager,
)
super.init(coroutineScope)
}
override suspend fun onStateChange(state: ReduxState) {
setupControlBarViewModel.update(
state.permissionState,
state.localParticipantState.cameraState,
@ -99,29 +97,29 @@ internal class SetupViewModel(
state.localParticipantState.videoStreamID,
)
audioDeviceListViewModel.update(
state.localParticipantState.audioState
state.localParticipantState.audioState,
)
errorInfoViewModel.updateCallStateError(state.errorState)
errorInfoViewModel.updateAudioFocusRejectedState(
state.audioSessionState.audioFocusStatus == AudioFocusStatus.REJECTED
state.audioSessionState.audioFocusStatus == AudioFocusStatus.REJECTED,
)
state.localParticipantState.cameraState.error?.let {
errorInfoViewModel.updateCallCompositeError(it)
}
setupGradientViewModel.update(
state.localParticipantState.videoStreamID,
state.localParticipantState.cameraState.operation
state.localParticipantState.cameraState.operation,
)
participantAvatarViewModel.update(
state.localParticipantState.videoStreamID,
state.permissionState
state.permissionState,
)
joinCallButtonHolderViewModel.update(
state.permissionState.audioPermissionState,
state.callState,
state.permissionState.cameraPermissionState,
state.localParticipantState.cameraState.operation,
state.localParticipantState.cameraState.camerasCount
state.localParticipantState.cameraState.camerasCount,
)
}
}

Просмотреть файл

@ -15,9 +15,9 @@ import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.ui.calling.error.CallStateError
import com.azure.android.communication.ui.calling.error.ErrorCode
import com.azure.android.communication.ui.calling.implementation.R
import com.azure.android.communication.ui.calling.models.CallCompositeEventCode
import com.google.android.material.snackbar.BaseTransientBottomBar.ANIMATION_MODE_FADE
import com.microsoft.fluentui.snackbar.Snackbar
@ -28,7 +28,10 @@ internal class ErrorInfoView(private val rootView: View) {
private lateinit var snackBar: Snackbar
private lateinit var snackBarTextView: TextView
fun start(viewLifecycleOwner: LifecycleOwner, snackBarViewModel: ErrorInfoViewModel) {
fun start(
viewLifecycleOwner: LifecycleOwner,
snackBarViewModel: ErrorInfoViewModel,
) {
initSnackBar()
viewLifecycleOwner.lifecycleScope.launch {
snackBarViewModel.getCallStateErrorStateFlow().collect {
@ -49,7 +52,7 @@ internal class ErrorInfoView(private val rootView: View) {
displaySnackBar(
null,
rootView.context.getText(R.string.azure_communication_ui_calling_call_video_fails_error)
.toString()
.toString(),
)
}
}
@ -66,12 +69,16 @@ internal class ErrorInfoView(private val rootView: View) {
snackBar.anchorView = null
}
private fun displaySnackBar(it: CallStateError?, message: String) {
val errorMessage = if (it != null) {
getErrorMessage(it)
} else {
message
}
private fun displaySnackBar(
it: CallStateError?,
message: String,
) {
val errorMessage =
if (it != null) {
getErrorMessage(it)
} else {
message
}
if (errorMessage.isBlank()) return
snackBarTextView.text = errorMessage
@ -101,46 +108,48 @@ internal class ErrorInfoView(private val rootView: View) {
}
private fun initSnackBar() {
snackBar = Snackbar.make(
rootView,
"",
Snackbar.LENGTH_INDEFINITE,
Snackbar.Style.REGULAR
).apply {
animationMode = ANIMATION_MODE_FADE
setAction(rootView.context!!.getText(R.string.azure_communication_ui_calling_snack_bar_button_dismiss)) {}
anchorView =
rootView.findViewById(R.id.azure_communication_ui_setup_join_call_button)
view.background.colorFilter = PorterDuffColorFilter(
ContextCompat.getColor(
rootView.context,
R.color.azure_communication_ui_calling_color_snack_bar_background
),
PorterDuff.Mode.SRC_IN
)
snackBarTextView = view.findViewById(R.id.snackbar_text)
snackBarTextView.setTextColor(
ContextCompat.getColor(
rootView.context,
R.color.azure_communication_ui_calling_color_snack_bar_text_color
)
)
view.findViewById<AppCompatButton>(R.id.snackbar_action).apply {
setTextColor(
snackBar =
Snackbar.make(
rootView,
"",
Snackbar.LENGTH_INDEFINITE,
Snackbar.Style.REGULAR,
).apply {
animationMode = ANIMATION_MODE_FADE
setAction(rootView.context!!.getText(R.string.azure_communication_ui_calling_snack_bar_button_dismiss)) {}
anchorView =
rootView.findViewById(R.id.azure_communication_ui_setup_join_call_button)
view.background.colorFilter =
PorterDuffColorFilter(
ContextCompat.getColor(
rootView.context,
R.color.azure_communication_ui_calling_color_snack_bar_background,
),
PorterDuff.Mode.SRC_IN,
)
snackBarTextView = view.findViewById(R.id.snackbar_text)
snackBarTextView.setTextColor(
ContextCompat.getColor(
rootView.context,
R.color.azure_communication_ui_calling_color_snack_bar_text_color
)
R.color.azure_communication_ui_calling_color_snack_bar_text_color,
),
)
view.findViewById<AppCompatButton>(R.id.snackbar_action).apply {
setTextColor(
ContextCompat.getColor(
rootView.context,
R.color.azure_communication_ui_calling_color_snack_bar_text_color,
),
)
isAllCaps = false
contentDescription =
rootView.context.getText(R.string.azure_communication_ui_calling_snack_bar_button_dismiss)
}
ViewCompat.setImportantForAccessibility(
view,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES,
)
isAllCaps = false
contentDescription =
rootView.context.getText(R.string.azure_communication_ui_calling_snack_bar_button_dismiss)
}
ViewCompat.setImportantForAccessibility(
view,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES
)
}
}
private fun View.accessibilityFocus(): View {

Просмотреть файл

@ -14,6 +14,7 @@ internal class ErrorInfoViewModel {
private val _callCompositeErrorFlow: MutableStateFlow<CallCompositeError?> = MutableStateFlow(null)
private val audioFocusRejectedFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
var callCompositeErrorFlow = _callCompositeErrorFlow
fun updateCallStateError(errorState: ErrorState) {
callStateErrorFlow.value = errorState.callStateError
}

Просмотреть файл

@ -13,9 +13,9 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.azure.android.communication.ui.calling.implementation.R
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import com.azure.android.communication.ui.calling.implementation.R
internal class JoinCallButtonHolderView : ConstraintLayout {
constructor(context: Context) : super(context)
@ -36,15 +36,16 @@ internal class JoinCallButtonHolderView : ConstraintLayout {
findViewById(R.id.azure_communication_ui_setup_start_call_button_text)
progressBar = findViewById(R.id.azure_communication_ui_setup_start_call_progress_bar)
joiningCallText = findViewById(R.id.azure_communication_ui_setup_start_call_joining_text)
setupJoinCallButton.background = ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_corner_radius_rectangle_4dp_primary_background
)
setupJoinCallButton.background =
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_corner_radius_rectangle_4dp_primary_background,
)
}
fun start(
viewLifecycleOwner: LifecycleOwner,
viewModel: JoinCallButtonHolderViewModel
viewModel: JoinCallButtonHolderViewModel,
) {
this.viewModel = viewModel
setupJoinCallButtonText.text = context.getString(R.string.azure_communication_ui_calling_setup_view_button_join_call)

Просмотреть файл

@ -20,9 +20,8 @@ import kotlinx.coroutines.flow.StateFlow
internal class JoinCallButtonHolderViewModel(
private val dispatch: (Action) -> Unit,
private val audioManager: AudioManager
private val audioManager: AudioManager,
) {
private lateinit var joinCallButtonEnabledFlow: MutableStateFlow<Boolean>
private var disableJoinCallButtonFlow = MutableStateFlow(false)
private lateinit var networkManager: NetworkManager
@ -57,7 +56,7 @@ internal class JoinCallButtonHolderViewModel(
MutableStateFlow(
audioPermissionState == PermissionStatus.GRANTED &&
cameraPermissionState != PermissionStatus.UNKNOWN &&
(camerasCount == 0 || cameraOperationalStatus != CameraOperationalStatus.PENDING)
(camerasCount == 0 || cameraOperationalStatus != CameraOperationalStatus.PENDING),
)
disableJoinCallButtonFlow.value = false
this.networkManager = networkManager

Просмотреть файл

@ -93,8 +93,8 @@ internal class PermissionWarningView : LinearLayout {
setupMissingImage.setImageDrawable(
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_ic_fluent_mic_off_24_filled_composite_button_enabled
)
R.drawable.azure_communication_ui_calling_ic_fluent_mic_off_24_filled_composite_button_enabled,
),
)
setupMissingText.setText(context.getString(R.string.azure_communication_ui_calling_setup_view_preview_area_audio_disabled))
} else if (!cameraPermissionGranted) {
@ -102,8 +102,8 @@ internal class PermissionWarningView : LinearLayout {
setupMissingImage.setImageDrawable(
ContextCompat.getDrawable(
context,
R.drawable.azure_communication_ui_calling_ic_fluent_video_off_24_filled_composite_button_enabled
)
R.drawable.azure_communication_ui_calling_ic_fluent_video_off_24_filled_composite_button_enabled,
),
)
setupMissingText.setText(context.getString(R.string.azure_communication_ui_calling_setup_view_preview_area_camera_disabled))
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше