diff --git a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/AzureCommunicationUICalling.xcodeproj/project.pbxproj b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/AzureCommunicationUICalling.xcodeproj/project.pbxproj index b3050a9c8..ddd8c5400 100644 --- a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/AzureCommunicationUICalling.xcodeproj/project.pbxproj +++ b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/AzureCommunicationUICalling.xcodeproj/project.pbxproj @@ -160,6 +160,8 @@ 529C34E328A4B25400ACD918 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529C34E228A4B25400ACD918 /* NetworkManager.swift */; }; 5312A5382B75E8ED001C92E1 /* SupportFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5312A5372B75E8ED001C92E1 /* SupportFormViewModelTests.swift */; }; 5318F62F2BEDFB0C00CAFA12 /* BottomDrawer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318F62E2BEDFB0C00CAFA12 /* BottomDrawer.swift */; }; + 532CB1BD2C54411F00C99510 /* AccessibilityAnnouncementManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 532CB1BC2C54411F00C99510 /* AccessibilityAnnouncementManager.swift */; }; + 532CB1C02C54482B00C99510 /* ParticipantsChangedHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 532CB1BF2C54482B00C99510 /* ParticipantsChangedHook.swift */; }; 532DF40B2AF9948F00E63301 /* CallCompositeUserReportedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 532DF40A2AF9948F00E63301 /* CallCompositeUserReportedError.swift */; }; 533D7AFC2B1965BC0021767F /* SupportFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 533D7AFB2B1965BC0021767F /* SupportFormView.swift */; }; 533D7B002B27BEAE0021767F /* SupportFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 533D7AFF2B27BEAE0021767F /* SupportFormViewModel.swift */; }; @@ -180,6 +182,7 @@ 53C1B7F72C2F88CA00E6DB4C /* ParticipantListVIew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53C1B7F62C2F88CA00E6DB4C /* ParticipantListVIew.swift */; }; 53D734D62C0AAD4700017EFB /* LeaveCallConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D734D52C0AAD4700017EFB /* LeaveCallConfirmationView.swift */; }; 53D734D82C0AAD9600017EFB /* LeaveCallConfirmationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D734D72C0AAD9600017EFB /* LeaveCallConfirmationViewModel.swift */; }; + 53F961562C583E5400D13050 /* AccessibilityAnnouncementManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F961552C583E5400D13050 /* AccessibilityAnnouncementManagerTests.swift */; }; 53F9FB872C387F8E009941FA /* ParticipantMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F9FB862C387F8E009941FA /* ParticipantMenuView.swift */; }; 5A1F4BCD2BD9756D00EA7B2D /* LeaveCallConfirmationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1F4BCA2BD9756C00EA7B2D /* LeaveCallConfirmationMode.swift */; }; 5A1F4BCE2BD9756D00EA7B2D /* CallScreenOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1F4BCB2BD9756C00EA7B2D /* CallScreenOptions.swift */; }; @@ -555,6 +558,8 @@ 529C34E228A4B25400ACD918 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; 5312A5372B75E8ED001C92E1 /* SupportFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportFormViewModelTests.swift; sourceTree = ""; }; 5318F62E2BEDFB0C00CAFA12 /* BottomDrawer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomDrawer.swift; sourceTree = ""; }; + 532CB1BC2C54411F00C99510 /* AccessibilityAnnouncementManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityAnnouncementManager.swift; sourceTree = ""; }; + 532CB1BF2C54482B00C99510 /* ParticipantsChangedHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantsChangedHook.swift; sourceTree = ""; }; 532DF40A2AF9948F00E63301 /* CallCompositeUserReportedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallCompositeUserReportedError.swift; sourceTree = ""; }; 533D7AFB2B1965BC0021767F /* SupportFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportFormView.swift; sourceTree = ""; }; 533D7AFF2B27BEAE0021767F /* SupportFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportFormViewModel.swift; sourceTree = ""; }; @@ -575,6 +580,7 @@ 53C1B7F62C2F88CA00E6DB4C /* ParticipantListVIew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantListVIew.swift; sourceTree = ""; }; 53D734D52C0AAD4700017EFB /* LeaveCallConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveCallConfirmationView.swift; sourceTree = ""; }; 53D734D72C0AAD9600017EFB /* LeaveCallConfirmationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveCallConfirmationViewModel.swift; sourceTree = ""; }; + 53F961552C583E5400D13050 /* AccessibilityAnnouncementManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityAnnouncementManagerTests.swift; sourceTree = ""; }; 53F9FB862C387F8E009941FA /* ParticipantMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantMenuView.swift; sourceTree = ""; }; 5A1F4BCA2BD9756C00EA7B2D /* LeaveCallConfirmationMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LeaveCallConfirmationMode.swift; sourceTree = ""; }; 5A1F4BCB2BD9756C00EA7B2D /* CallScreenOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallScreenOptions.swift; sourceTree = ""; }; @@ -1076,6 +1082,7 @@ 1F94DAE22673F72000691D1E /* Manager */ = { isa = PBXGroup; children = ( + 532CB1BE2C54481900C99510 /* AccessibilityAnnouncementHooks */, A874EDC3266152AA003C7D92 /* AppLifeCycleManager.swift */, 98546EB926DEF24D0069B246 /* AudioDeviceType.swift */, 98546EB526D9B9990069B246 /* AudioSessionManager.swift */, @@ -1089,6 +1096,7 @@ A464810529F33A43001B80A3 /* CallStateManager.swift */, A464810929F34A06001B80A3 /* CompositeExitManager.swift */, C89FCE5F2C0FAF5D00983444 /* CapabilitiesManager.swift */, + 532CB1BC2C54411F00C99510 /* AccessibilityAnnouncementManager.swift */, ); path = Manager; sourceTree = ""; @@ -1236,6 +1244,14 @@ path = ViewComponents; sourceTree = ""; }; + 532CB1BE2C54481900C99510 /* AccessibilityAnnouncementHooks */ = { + isa = PBXGroup; + children = ( + 532CB1BF2C54482B00C99510 /* ParticipantsChangedHook.swift */, + ); + path = AccessibilityAnnouncementHooks; + sourceTree = ""; + }; 533D7AFA2B1965910021767F /* SupportForm */ = { isa = PBXGroup; children = ( @@ -1283,6 +1299,14 @@ path = MoreCallOptions; sourceTree = ""; }; + 53797AEB2C583B1200EE12DF /* Manager */ = { + isa = PBXGroup; + children = ( + 53F961552C583E5400D13050 /* AccessibilityAnnouncementManagerTests.swift */, + ); + path = Manager; + sourceTree = ""; + }; 53C1B7F42C2F882F00E6DB4C /* ParticipantsList */ = { isa = PBXGroup; children = ( @@ -1464,6 +1488,7 @@ A830C310264D815E00766E3D /* Presentation */ = { isa = PBXGroup; children = ( + 53797AEB2C583B1200EE12DF /* Manager */, 50B390D427D915AB0010A2ED /* Provider */, 503E361A26CC2CFE00158CB4 /* Factories */, 1F09A10B26BA484000BACED7 /* Calling */, @@ -1975,6 +2000,7 @@ A48BE79129FB424F009B2A46 /* CallStateManagerTests.swift in Sources */, FAD845422819FC8E007DAFE1 /* RemoteParticipantsManagerTests.swift in Sources */, A830C306264C3B6400766E3D /* AppStateReducerTests.swift in Sources */, + 53F961562C583E5400D13050 /* AccessibilityAnnouncementManagerTests.swift in Sources */, 88BC24C12832D29D00818446 /* AvatarManagerTests.swift in Sources */, B26F4B092ABA2F6B00753B6C /* BottomToastViewModelTests.swift in Sources */, 05317E2629A2E1FB00149F57 /* CallHistoryServiceTests.swift in Sources */, @@ -2078,6 +2104,7 @@ FAD20A8D292834500063D8A9 /* CancelBag.swift in Sources */, 50FA460C26829162001844AC /* IconWithLabelButton.swift in Sources */, 756F596E2965F8EA00BAFBF7 /* Store.swift in Sources */, + 532CB1C02C54482B00C99510 /* ParticipantsChangedHook.swift in Sources */, B2B1DB3F2ABE35DA003B00DB /* MessageBarDiagnosticViewModel.swift in Sources */, 1B7ABF82278CFFC400D79DF7 /* ZoomableVideoRenderView.swift in Sources */, 53D734D62C0AAD4700017EFB /* LeaveCallConfirmationView.swift in Sources */, @@ -2138,6 +2165,7 @@ 88701EB8274C29C600660EAB /* CallingMiddlewareHandlerExtension.swift in Sources */, 11F792E329AFCFB100EAABB8 /* LoadingOverlayViewModel.swift in Sources */, A48D18022BFD84D600FD994E /* IncomingCallError.swift in Sources */, + 532CB1BD2C54411F00C99510 /* AccessibilityAnnouncementManager.swift in Sources */, 1F2BED992631FA6C00D98266 /* PermissionState.swift in Sources */, 1F94DADF2673E94F00691D1E /* CallComposite.swift in Sources */, 534A5DD82C40AE0B002EB508 /* DrawerBodyTextView.swift in Sources */, diff --git a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/CallComposite.swift b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/CallComposite.swift index eb296cda7..b62b90d0c 100644 --- a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/CallComposite.swift +++ b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/CallComposite.swift @@ -68,6 +68,7 @@ public class CallComposite { private var audioSessionManager: AudioSessionManagerProtocol? private var remoteParticipantsManager: RemoteParticipantsManagerProtocol? private var avatarViewManager: AvatarViewManagerProtocol? + private var accessibilityAnnouncementManager: AccessibilityAnnouncementManager? private var customCallingSdkWrapper: CallingSDKWrapperProtocol? private var debugInfoManager: DebugInfoManagerProtocol? private var pipManager: PipManagerProtocol? @@ -548,6 +549,12 @@ and launch(locator: JoinLocator, localOptions: LocalOptions? = nil) instead. localParticipantViewData: localOptions?.participantViewData ) self.avatarViewManager = avatarViewManager + + self.accessibilityAnnouncementManager = AccessibilityAnnouncementManager( + store: store, + accessibilityProvider: accessibilityProvider, + localizationProvider: localizationProvider) + let audioSessionManager = AudioSessionManager(store: store, logger: logger, isCallKitEnabled: callKitOptions != nil) @@ -622,6 +629,7 @@ and launch(locator: JoinLocator, localOptions: LocalOptions? = nil) instead. self.exitManager = nil self.callingSDKWrapper?.dispose() self.callingSDKWrapper = nil + self.accessibilityAnnouncementManager = nil } private func disposeSDKWrappers() { diff --git a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/Manager/AccessibilityAnnouncementHooks/ParticipantsChangedHook.swift b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/Manager/AccessibilityAnnouncementHooks/ParticipantsChangedHook.swift new file mode 100644 index 000000000..93078fd65 --- /dev/null +++ b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/Manager/AccessibilityAnnouncementHooks/ParticipantsChangedHook.swift @@ -0,0 +1,83 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// + +import Foundation + +/* Handles detection of Participants added/removed, and returns what to announce */ +internal class ParticipantsChangedHook: AccessibilityAnnouncementHookProtocol { + func shouldAnnounce(oldState: AppState, newState: AppState) -> Bool { + let callingState = newState.callingState + let isConnecting = callingState.status == .connecting + let isRinging = callingState.status == .ringing + let isConnected = callingState.status == .connected + let isRemoteHold = callingState.status == .remoteHold + let isOneToNOutgoing = callingState.callType == .oneToNOutgoing + let isConnectingOrRinging = isRinging || isConnecting + let oldParticipants = oldState.remoteParticipantsState.participantInfoList + let newParticipants = newState.remoteParticipantsState.participantInfoList + let oldParticipantIDs = Set(oldParticipants.map { $0.userIdentifier }) + let newParticipantIDs = Set(newParticipants.map { $0.userIdentifier }) + + return (isConnected || + isRemoteHold || + (isOneToNOutgoing && isConnectingOrRinging)) + && oldParticipantIDs != newParticipantIDs + } + + func announcement(oldState: AppState, + newState: AppState, + localizationProvider: LocalizationProviderProtocol) -> String { + let oldParticipants = oldState.remoteParticipantsState.participantInfoList + let newParticipants = newState.remoteParticipantsState.participantInfoList + + let oldParticipantIDs = Set(oldParticipants.map { $0.userIdentifier }) + let newParticipantIDs = Set(newParticipants.map { $0.userIdentifier }) + + let removedParticipantIDs = oldParticipantIDs.subtracting(newParticipantIDs) + let addedParticipantIDs = newParticipantIDs.subtracting(oldParticipantIDs) + + let removedParticipants = oldParticipants.filter { removedParticipantIDs.contains($0.userIdentifier) } + let addedParticipants = newParticipants.filter { addedParticipantIDs.contains($0.userIdentifier) } + + var announcements = [String]() + + // 4 Branches. Add/Remove x Single/Multiple + if !removedParticipants.isEmpty { + if removedParticipants.count == 1 { + let removedParticipant = removedParticipants.first! + announcements.append( + localizationProvider + .getLocalizedString( + .onePersonLeft, + removedParticipant.displayName)) + } else { + announcements.append( + localizationProvider + .getLocalizedString( + .multiplePeopleLeft, + removedParticipants.count)) + } + } + + if !addedParticipants.isEmpty { + if addedParticipants.count == 1 { + let addedParticipant = addedParticipants.first! + announcements.append( + localizationProvider + .getLocalizedString( + .onePersonJoined, + addedParticipant.displayName)) + } else { + announcements.append( + localizationProvider + .getLocalizedString( + .multiplePeopleJoined, + addedParticipants.count)) + } + } + + return announcements.joined(separator: " ") + } +} diff --git a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/Manager/AccessibilityAnnouncementManager.swift b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/Manager/AccessibilityAnnouncementManager.swift new file mode 100644 index 000000000..be1eb6cd5 --- /dev/null +++ b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/Manager/AccessibilityAnnouncementManager.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// + +import Foundation +import Combine + +internal protocol AccessibilityAnnouncementManagerProtocol {} + +// Interface for Hooks to Implement (A hook is a single use-case of Accessibility Announcement +internal protocol AccessibilityAnnouncementHookProtocol { + func shouldAnnounce(oldState: AppState, + newState: AppState) -> Bool + func announcement(oldState: AppState, + newState: AppState, + localizationProvider: LocalizationProviderProtocol) -> String +} + +/* Handles watching state and making relevant announcements*/ +internal class AccessibilityAnnouncementManager: AccessibilityAnnouncementManagerProtocol { + private let store: Store + private let accessibilityProvider: AccessibilityProviderProtocol + private let localizationProvider: LocalizationProviderProtocol + private var lastState: AppState + private let hooks = [ParticipantsChangedHook()] + + var cancellables = Set() + + init(store: Store, + accessibilityProvider: AccessibilityProviderProtocol, + localizationProvider: LocalizationProviderProtocol) { + self.store = store + self.lastState = store.state + self.accessibilityProvider = accessibilityProvider + self.localizationProvider = localizationProvider + + store.$state + .receive(on: RunLoop.main) + .sink { [weak self] state in + self?.receive(state) + }.store(in: &cancellables) + } + + private func receive(_ state: AppState) { + for hook in hooks where hook.shouldAnnounce(oldState: lastState, newState: state) { + accessibilityProvider.postQueuedAnnouncement(hook.announcement( + oldState: lastState, + newState: state, + localizationProvider: localizationProvider)) + } + lastState = state + } +} diff --git a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/SwiftUI/Calling/Grid/ParticipantGridViewModel.swift b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/SwiftUI/Calling/Grid/ParticipantGridViewModel.swift index 9d973b8a3..c143f81b8 100644 --- a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/SwiftUI/Calling/Grid/ParticipantGridViewModel.swift +++ b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/SwiftUI/Calling/Grid/ParticipantGridViewModel.swift @@ -75,14 +75,6 @@ class ParticipantGridViewModel: ObservableObject { updateCellViewModel(for: orderedInfoModelArr, lifeCycleState: lifeCycleState) displayedParticipantInfoModelArr = orderedInfoModelArr - if callingState.status == .connected - || callingState.status == .remoteHold - || (callType == .oneToNOutgoing - && ( callingState.status == .connecting || callingState.status == .ringing)) { - // announce participants list changes only if the user is already connected to the call - postParticipantsListUpdateAccessibilityAnnouncements(removedModels: removedModels, - addedModels: addedModels) - } if gridsCount != displayedParticipantInfoModelArr.count { gridsCount = displayedParticipantInfoModelArr.count } @@ -214,26 +206,4 @@ class ParticipantGridViewModel: ObservableObject { participantsCellViewModelArr = newCellViewModelArr } - - private func postParticipantsListUpdateAccessibilityAnnouncements(removedModels: [ParticipantInfoModel], - addedModels: [ParticipantInfoModel]) { - if !removedModels.isEmpty { - if removedModels.count == 1 { - accessibilityProvider.postQueuedAnnouncement( - localizationProvider.getLocalizedString(.onePersonLeft, removedModels.first!.displayName)) - } else { - accessibilityProvider.postQueuedAnnouncement( - localizationProvider.getLocalizedString(.multiplePeopleLeft, removedModels.count)) - } - } - if !addedModels.isEmpty { - if addedModels.count == 1 { - accessibilityProvider.postQueuedAnnouncement( - localizationProvider.getLocalizedString(.onePersonJoined, addedModels.first!.displayName)) - } else { - accessibilityProvider.postQueuedAnnouncement( - localizationProvider.getLocalizedString(.multiplePeopleJoined, addedModels.count)) - } - } - } } diff --git a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Redux/State/AppState.swift b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Redux/State/AppState.swift index 279c1c1e3..3b1c74f08 100644 --- a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Redux/State/AppState.swift +++ b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Redux/State/AppState.swift @@ -6,6 +6,7 @@ import Foundation struct AppState { + let callingState: CallingState let permissionState: PermissionState let localUserState: LocalUserState diff --git a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Redux/State/CallingState.swift b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Redux/State/CallingState.swift index 0f39e8510..b8c3a2d3a 100644 --- a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Redux/State/CallingState.swift +++ b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Redux/State/CallingState.swift @@ -32,6 +32,7 @@ enum RecordingStatus: Equatable { } struct CallingState: Equatable { + let callType: CompositeCallType let status: CallingStatus let operationStatus: OperationStatus let callId: String? @@ -44,7 +45,8 @@ struct CallingState: Equatable { let callEndReasonCode: Int? let callEndReasonSubCode: Int? - init(status: CallingStatus = .none, + init(callType: CompositeCallType = .groupCall, + status: CallingStatus = .none, operationStatus: OperationStatus = .none, callId: String? = nil, isRecordingActive: Bool = false, @@ -55,6 +57,7 @@ struct CallingState: Equatable { recordingStatus: RecordingStatus = RecordingStatus.off, transcriptionStatus: RecordingStatus = RecordingStatus.off, isRecorcingTranscriptionBannedDismissed: Bool = false) { + self.callType = callType self.status = status self.operationStatus = operationStatus self.callId = callId diff --git a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Redux/StoreExtensions.swift b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Redux/StoreExtensions.swift index 68d4991c3..a9acd0352 100644 --- a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Redux/StoreExtensions.swift +++ b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Redux/StoreExtensions.swift @@ -31,7 +31,9 @@ extension Store where State == AppState, Action == AzureCommunicationUICalling.A let localUserState = LocalUserState(displayName: displayName) let callingState = skipSetupScreen ?? false ? - CallingState(operationStatus: .skipSetupRequested) : CallingState() + CallingState( + callType: callType, + operationStatus: .skipSetupRequested) : CallingState(callType: callType) let navigationStatus: NavigationStatus = skipSetupScreen ?? false ? .inCall : .setup let navigationState = NavigationState(status: navigationStatus) return .init( diff --git a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Tests/Presentation/Calling/ParticipantGridsViewModelTests.swift b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Tests/Presentation/Calling/ParticipantGridsViewModelTests.swift index 52623a010..61a974cba 100644 --- a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Tests/Presentation/Calling/ParticipantGridsViewModelTests.swift +++ b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Tests/Presentation/Calling/ParticipantGridsViewModelTests.swift @@ -339,68 +339,6 @@ class ParticipantGridViewModelTests: XCTestCase { XCTAssertEqual(sut.participantsCellViewModelArr.count, expectedCount) } - func test_participantGridsViewModel_updateParticipantsState_when_newParticipantsJoined_then_participantsJoinedAnnouncementPosted() { - let expectation = XCTestExpectation(description: "Announcement expection") - let state = makeRemoteParticipantState(count: 2) - let callingState = CallingState(status: .connected) - let expectedAnnouncement = "2 participants joined the meeting" - let accessibilityProvider = AccessibilityProviderMocking() - let localizationProvider = LocalizationProvider(logger: LoggerMocking()) - accessibilityProvider.postQueuedAnnouncementBlock = { message in - XCTAssertEqual(message, expectedAnnouncement) - expectation.fulfill() - } - let sut = makeSUT(accessibilityProvider: accessibilityProvider, - localizationProvider: localizationProvider) - sut.update(callingState: callingState, - remoteParticipantsState: state, - visibilityState: VisibilityState(currentStatus: .visible), - lifeCycleState: LifeCycleState(currentStatus: .foreground)) - wait(for: [expectation], timeout: 1) - } - - func test_participantGridsViewModel_updateParticipantsState_when_newParticipantsRinging_then_participantsJoinedAnnouncementPosted() { - let expectation = XCTestExpectation(description: "Announcement expection") - let state = makeRemoteParticipantState(count: 2) - let callingState = CallingState(status: .ringing) - let expectedAnnouncement = "2 participants joined the meeting" - let accessibilityProvider = AccessibilityProviderMocking() - let localizationProvider = LocalizationProvider(logger: LoggerMocking()) - accessibilityProvider.postQueuedAnnouncementBlock = { message in - XCTAssertEqual(message, expectedAnnouncement) - expectation.fulfill() - } - let sut = makeSUT(callType: CompositeCallType.oneToNOutgoing, - accessibilityProvider: accessibilityProvider, - localizationProvider: localizationProvider) - sut.update(callingState: callingState, - remoteParticipantsState: state, - visibilityState: VisibilityState(currentStatus: .visible), - lifeCycleState: LifeCycleState(currentStatus: .foreground)) - wait(for: [expectation], timeout: 1) - } - - func test_participantGridsViewModel_updateParticipantsState_when_newParticipantsConnecting_then_participantsJoinedAnnouncementPosted() { - let expectation = XCTestExpectation(description: "Announcement expection") - let state = makeRemoteParticipantState(count: 2) - let callingState = CallingState(status: .connecting) - let expectedAnnouncement = "2 participants joined the meeting" - let accessibilityProvider = AccessibilityProviderMocking() - let localizationProvider = LocalizationProvider(logger: LoggerMocking()) - accessibilityProvider.postQueuedAnnouncementBlock = { message in - XCTAssertEqual(message, expectedAnnouncement) - expectation.fulfill() - } - let sut = makeSUT(callType: CompositeCallType.oneToNOutgoing, - accessibilityProvider: accessibilityProvider, - localizationProvider: localizationProvider) - sut.update(callingState: callingState, - remoteParticipantsState: state, - visibilityState: VisibilityState(currentStatus: .visible), - lifeCycleState: LifeCycleState(currentStatus: .foreground)) - wait(for: [expectation], timeout: 1) - } - func test_participantGridsViewModel_updateParticipantsState_when_viewModelLastUpdateTimeStampSame_then_noUpdateRemoteParticipantCellViewModel() { let date = Calendar.current.date( byAdding: .minute, @@ -424,104 +362,6 @@ class ParticipantGridViewModelTests: XCTestCase { lifeCycleState: LifeCycleState(currentStatus: .foreground)) XCTAssertEqual(sut.participantsCellViewModelArr.count, expectedCount) } - func test_participantGridsViewModel_updateParticipantsState_when_newParticipantJoined_then_participantJoinedAnnouncementPosted() { - let expectation = XCTestExpectation(description: "Announcement expection") - let state = makeRemoteParticipantState(count: 1) - let callingState = CallingState(status: .connected) - let displayName = state.participantInfoList.first!.displayName - let accessibilityProvider = AccessibilityProviderMocking() - let localizationProvider = LocalizationProvider(logger: LoggerMocking()) - let expectedAnnouncement = "\(displayName) joined the meeting" - accessibilityProvider.postQueuedAnnouncementBlock = { message in - XCTAssertEqual(message, expectedAnnouncement) - expectation.fulfill() - } - let sut = makeSUT(accessibilityProvider: accessibilityProvider, - localizationProvider: localizationProvider) - sut.update(callingState: callingState, - remoteParticipantsState: state, - visibilityState: VisibilityState(currentStatus: .visible), - lifeCycleState: LifeCycleState(currentStatus: .foreground)) - wait(for: [expectation], timeout: 1) - } - - func test_participantGridsViewModel_updateParticipantsState_when_participantsLeft_then_participantsLeftAnnouncementPosted() { - let expectation = XCTestExpectation(description: "Announcement expection") - let state = makeRemoteParticipantState(count: 4) - let callingState = CallingState(status: .connected) - let accessibilityProvider = AccessibilityProviderMocking() - let localizationProvider = LocalizationProvider(logger: LoggerMocking()) - let expectedAnnouncement = "2 participants left the meeting" - let sut = makeSUT(accessibilityProvider: accessibilityProvider, - localizationProvider: localizationProvider) - sut.update(callingState: callingState, - remoteParticipantsState: state, - visibilityState: VisibilityState(currentStatus: .visible), - lifeCycleState: LifeCycleState(currentStatus: .foreground)) - accessibilityProvider.postQueuedAnnouncementBlock = { message in - XCTAssertEqual(message, expectedAnnouncement) - expectation.fulfill() - } - let updatedState = RemoteParticipantsState(participantInfoList: state.participantInfoList.dropLast(2), - lastUpdateTimeStamp: Date()) - sut.update(callingState: callingState, - remoteParticipantsState: updatedState, - visibilityState: VisibilityState(currentStatus: .visible), - lifeCycleState: LifeCycleState(currentStatus: .foreground)) - wait(for: [expectation], timeout: 1) - } - - func test_participantGridsViewModel_updateParticipantsState_when_participantLeft_then_participantLeftAnnouncementPosted() { - let expectation = XCTestExpectation(description: "Announcement expection") - let state = makeRemoteParticipantState(count: 1) - let callingState = CallingState(status: .connected) - let displayName = state.participantInfoList.first!.displayName - let accessibilityProvider = AccessibilityProviderMocking() - let localizationProvider = LocalizationProvider(logger: LoggerMocking()) - let expectedAnnouncement = "\(displayName) left the meeting" - let sut = makeSUT(accessibilityProvider: accessibilityProvider, - localizationProvider: localizationProvider) - sut.update(callingState: callingState, - remoteParticipantsState: state, - visibilityState: VisibilityState(currentStatus: .visible), - lifeCycleState: LifeCycleState(currentStatus: .foreground)) - accessibilityProvider.postQueuedAnnouncementBlock = { message in - XCTAssertEqual(message, expectedAnnouncement) - expectation.fulfill() - } - let updatedState = RemoteParticipantsState(participantInfoList: state.participantInfoList.dropLast(2), - lastUpdateTimeStamp: Date()) - sut.update(callingState: callingState, - remoteParticipantsState: updatedState, - visibilityState: VisibilityState(currentStatus: .visible), - lifeCycleState: LifeCycleState(currentStatus: .foreground)) - wait(for: [expectation], timeout: 1) - } - - func test_participantGridsViewModel_updateParticipantsState_when_participantsListChanged_then_participantLeftAndJoinedAnnouncementPosted() { - let expectation = XCTestExpectation(description: "Announcement expection") - expectation.expectedFulfillmentCount = 2 - expectation.assertForOverFulfill = true - let state = makeRemoteParticipantState(count: 2) - let callingState = CallingState(status: .connected) - let accessibilityProvider = AccessibilityProviderMocking() - let localizationProvider = LocalizationProvider(logger: LoggerMocking()) - let sut = makeSUT(accessibilityProvider: accessibilityProvider, - localizationProvider: localizationProvider) - sut.update(callingState: callingState, - remoteParticipantsState: state, - visibilityState: VisibilityState(currentStatus: .visible), - lifeCycleState: LifeCycleState(currentStatus: .foreground)) - accessibilityProvider.postQueuedAnnouncementBlock = { _ in - expectation.fulfill() - } - let updatedState = makeRemoteParticipantState(count: 3) - sut.update(callingState: callingState, - remoteParticipantsState: updatedState, - visibilityState: VisibilityState(currentStatus: .visible), - lifeCycleState: LifeCycleState(currentStatus: .foreground)) - wait(for: [expectation], timeout: 1) - } // MARK: GridsViewType func test_participantGridsViewModel_init_then_gridsCountZero() { diff --git a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Tests/Presentation/Manager/AccessibilityAnnouncementManagerTests.swift b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Tests/Presentation/Manager/AccessibilityAnnouncementManagerTests.swift new file mode 100644 index 000000000..7575acfe1 --- /dev/null +++ b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Tests/Presentation/Manager/AccessibilityAnnouncementManagerTests.swift @@ -0,0 +1,173 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// + +import Foundation +import SwiftUI +import XCTest + +@testable import AzureCommunicationUICalling + +class AccessibilityAnnouncementManagerTests: XCTestCase { + private var cancellable: CancelBag! + private var localizationProvider: LocalizationProviderMocking! + private var storeFactory: StoreFactoryMocking! + private var logger: LoggerMocking! + private var factoryMocking: CompositeViewModelFactoryMocking! + private let pA = ParticipantInfoModelBuilder.get( + participantIdentifier: "T1", + displayName: "Test1" + ) + + private let pB = ParticipantInfoModelBuilder.get( + participantIdentifier: "T2", + displayName: "Test2" + ) + + override func setUp() { + super.setUp() + cancellable = CancelBag() + localizationProvider = LocalizationProviderMocking() + storeFactory = StoreFactoryMocking() + logger = LoggerMocking() + factoryMocking = CompositeViewModelFactoryMocking( + logger: logger, store: storeFactory.store, + avatarManager: AvatarViewManagerMocking( + store: storeFactory.store, + localParticipantViewData: nil)) + } + + func test_participantGridsViewModel_updateParticipantsState_when_newParticipantsJoined_then_participantsJoinedAnnouncementPosted() { + let expectation = XCTestExpectation(description: "Announcement expection") + let expectedAnnouncement = "2 participants joined the meeting" + let accessibilityProvider = AccessibilityProviderMocking() + let localizationProvider = LocalizationProvider(logger: LoggerMocking()) + + accessibilityProvider.postQueuedAnnouncementBlock = { message in + XCTAssertEqual(message, expectedAnnouncement) + expectation.fulfill() + } + storeFactory.setState(makeState(count: 0)) + let sut = makeSUT(accessibilityProvider: accessibilityProvider, + localizationProvider: localizationProvider) + storeFactory.setState(makeState(count: 2)) + wait(for: [expectation], timeout: 1) + } + + func test_participantGridsViewModel_updateParticipantsState_when_newParticipantsRinging_then_participantsJoinedAnnouncementPosted() { + let expectation = XCTestExpectation(description: "Announcement expection") + let expectedAnnouncement = "2 participants joined the meeting" + let accessibilityProvider = AccessibilityProviderMocking() + let localizationProvider = LocalizationProvider(logger: LoggerMocking()) + accessibilityProvider.postQueuedAnnouncementBlock = { message in + XCTAssertEqual(message, expectedAnnouncement) + expectation.fulfill() + } + storeFactory.setState(makeState(count: 0, callType: .oneToNOutgoing, callStatus: .ringing)) + let sut = makeSUT(accessibilityProvider: accessibilityProvider, + localizationProvider: localizationProvider) + storeFactory.setState(makeState(count: 2, callType: .oneToNOutgoing, callStatus: .ringing)) + wait(for: [expectation], timeout: 1) + } + + func test_participantGridsViewModel_updateParticipantsState_when_newParticipantsConnecting_then_participantsJoinedAnnouncementPosted() { + let expectation = XCTestExpectation(description: "Announcement expection") + let expectedAnnouncement = "2 participants joined the meeting" + let accessibilityProvider = AccessibilityProviderMocking() + let localizationProvider = LocalizationProvider(logger: LoggerMocking()) + accessibilityProvider.postQueuedAnnouncementBlock = { message in + XCTAssertEqual(message, expectedAnnouncement) + expectation.fulfill() + } + storeFactory.setState(makeState(count: 0, callType: .oneToNOutgoing, callStatus: .connecting)) + let sut = makeSUT(accessibilityProvider: accessibilityProvider, + localizationProvider: localizationProvider) + storeFactory.setState(makeState(count: 2, callType: .oneToNOutgoing, callStatus: .connecting)) + wait(for: [expectation], timeout: 1) + } + + func test_participantGridsViewModel_updateParticipantsState_when_newParticipantJoined_then_participantJoinedAnnouncementPosted() { + let expectation = XCTestExpectation(description: "Announcement expection") + let state = makeRemoteParticipantState(count: 1) + let displayName = state.participantInfoList.first!.displayName + let accessibilityProvider = AccessibilityProviderMocking() + let localizationProvider = LocalizationProvider(logger: LoggerMocking()) + let expectedAnnouncement = "\(displayName) joined the meeting" + + accessibilityProvider.postQueuedAnnouncementBlock = { message in + XCTAssertEqual(message, expectedAnnouncement) + expectation.fulfill() + } + storeFactory.setState(makeState(count: 0)) + let sut = makeSUT(accessibilityProvider: accessibilityProvider, localizationProvider: localizationProvider) + storeFactory.setState(makeState(count: 1)) + wait(for: [expectation], timeout: 1) + } + + func test_participantGridsViewModel_updateParticipantsState_when_newParticipantJoined_then_participantLeftAnnouncementPosted() { + let expectation = XCTestExpectation(description: "Announcement expection") + let state = makeRemoteParticipantState(count: 1) + let displayName = state.participantInfoList.first!.displayName + let accessibilityProvider = AccessibilityProviderMocking() + let localizationProvider = LocalizationProvider(logger: LoggerMocking()) + let expectedAnnouncement = "\(displayName) left the meeting" + + accessibilityProvider.postQueuedAnnouncementBlock = { message in + XCTAssertEqual(message, expectedAnnouncement) + expectation.fulfill() + } + storeFactory.setState(makeState(count: 1)) + let sut = makeSUT(accessibilityProvider: accessibilityProvider, localizationProvider: localizationProvider) + storeFactory.setState(makeState(count: 0)) + wait(for: [expectation], timeout: 1) + } + + func test_participantGridsViewModel_updateParticipantsState_when_participantsLeft_then_participantsLeftAnnouncementPosted() { + let expectation = XCTestExpectation(description: "Announcement expection") + let accessibilityProvider = AccessibilityProviderMocking() + let localizationProvider = LocalizationProvider(logger: LoggerMocking()) + let expectedAnnouncement = "2 participants left the meeting" + storeFactory.setState(makeState(count: 2)) + let sut = makeSUT(accessibilityProvider: accessibilityProvider, localizationProvider: localizationProvider) + storeFactory.setState(makeState(count: 0)) + + accessibilityProvider.postQueuedAnnouncementBlock = { message in + XCTAssertEqual(message, expectedAnnouncement) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1) + } + +} + +extension AccessibilityAnnouncementManagerTests { + func makeSUT( + accessibilityProvider: AccessibilityProviderProtocol, + localizationProvider: LocalizationProviderProtocol + ) -> AccessibilityAnnouncementManager { + // Force the Store into "Connected" + return AccessibilityAnnouncementManager( + store: storeFactory.store, + accessibilityProvider: accessibilityProvider, + localizationProvider: localizationProvider) + } + + private func makeRemoteParticipantState( + count: Int = 1, + lastUpdatedTimeStamp: Date = Date(), + dominantSpeakersModifiedTimestamp: Date = Date()) -> RemoteParticipantsState { + return RemoteParticipantsState(participantInfoList: ParticipantInfoModelBuilder.getArray(count: count), + lastUpdateTimeStamp: lastUpdatedTimeStamp, + dominantSpeakersModifiedTimestamp: dominantSpeakersModifiedTimestamp) + } + + func makeState(count: Int = 1, + callType: CompositeCallType = .groupCall, + callStatus: CallingStatus = .connected) -> AppState { + return AppState( + callingState: CallingState(callType: callType, status: callStatus), + remoteParticipantsState: makeRemoteParticipantState(count: count) + ) + } +} diff --git a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Tests/Service/CallHistoryRepositoryTests.swift.plist b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Tests/Service/CallHistoryRepositoryTests.swift.plist index d9547b3a5..dd9cdb787 100644 Binary files a/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Tests/Service/CallHistoryRepositoryTests.swift.plist and b/AzureCommunicationUI/sdk/AzureCommunicationUICalling/Tests/Service/CallHistoryRepositoryTests.swift.plist differ