From 6f8de0b2c63c7bc749adb0141e32c3abc73524a9 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 10 Jun 2024 11:51:19 +0200 Subject: [PATCH] Element Call ringing notifications (#2978) - Add `ActiveCallManager` to handle incoming and ongoing calls. - Add ringing call notifications with full screen intents and missed call ones as part of the 'conversation' notifications. --------- Co-authored-by: ElementBot --- .../android/appconfig/ElementCallConfig.kt | 8 + changelog.d/2894.feature | 1 + features/call/api/build.gradle.kts | 31 +++ .../android/features/call/api}/CallType.kt | 4 +- .../call/api/ElementCallEntryPoint.kt | 53 +++++ features/call/{ => impl}/build.gradle.kts | 16 +- .../{ => impl}/src/main/AndroidManifest.xml | 24 ++- .../call/impl/DefaultElementCallEntryPoint.kt | 69 ++++++ .../features/call/impl}/data/WidgetMessage.kt | 2 +- .../features/call/impl}/di/CallBindings.kt | 8 +- .../notifications/CallNotificationData.kt | 37 ++++ .../RingingCallNotificationCreator.kt | 130 +++++++++++ .../receivers/DeclineCallBroadcastReceiver.kt | 37 ++++ .../impl/services}/CallForegroundService.kt | 34 ++- .../call/impl}/ui/CallScreenEvents.kt | 7 +- .../call/impl}/ui/CallScreenPresenter.kt | 27 ++- .../features/call/impl}/ui/CallScreenState.kt | 2 +- .../features/call/impl}/ui/CallScreenView.kt | 6 +- .../call/impl}/ui/ElementCallActivity.kt | 48 ++--- .../call/impl/ui/IncomingCallActivity.kt | 96 +++++++++ .../call/impl/ui/IncomingCallScreen.kt | 192 +++++++++++++++++ .../call/impl/utils/ActiveCallManager.kt | 202 ++++++++++++++++++ .../call/impl}/utils/CallIntentDataParser.kt | 2 +- .../call/impl}/utils/CallWidgetProvider.kt | 2 +- .../impl}/utils/DefaultCallWidgetProvider.kt | 2 +- .../call/impl/utils/IntentProvider.kt | 42 ++++ .../utils/WebViewWidgetMessageInterceptor.kt | 4 +- .../impl}/utils/WidgetMessageInterceptor.kt | 2 +- .../impl}/utils/WidgetMessageSerializer.kt | 4 +- .../src/main/res/values-be/translations.xml | 0 .../src/main/res/values-cs/translations.xml | 0 .../src/main/res/values-de/translations.xml | 0 .../src/main/res/values-es/translations.xml | 0 .../src/main/res/values-fr/translations.xml | 0 .../src/main/res/values-hu/translations.xml | 0 .../src/main/res/values-in/translations.xml | 0 .../src/main/res/values-it/translations.xml | 0 .../src/main/res/values-ka/translations.xml | 0 .../src/main/res/values-pt/translations.xml | 0 .../src/main/res/values-ro/translations.xml | 0 .../src/main/res/values-ru/translations.xml | 0 .../src/main/res/values-sk/translations.xml | 0 .../src/main/res/values-sv/translations.xml | 0 .../src/main/res/values-uk/translations.xml | 0 .../main/res/values-zh-rTW/translations.xml | 0 .../src/main/res/values-zh/translations.xml | 0 .../src/main/res/values/do_not_translate.xml | 0 .../src/main/res/values/localazy.xml | 1 + .../call/DefaultElementCallEntryPointTest.kt | 77 +++++++ .../features/call/MapWebkitPermissionsTest.kt | 2 +- .../RingingCallNotificationCreatorTest.kt | 97 +++++++++ .../call/ui/CallScreenPresenterTest.kt | 10 +- .../call/ui/FakeCallScreenNavigator.kt | 2 + .../call/utils/CallIntentDataParserTest.kt | 1 + .../utils/DefaultActiveCallManagerTest.kt | 191 +++++++++++++++++ .../utils/DefaultCallWidgetProviderTest.kt | 1 + .../call/utils/FakeActiveCallManager.kt | 53 +++++ .../call/utils/FakeCallWidgetProvider.kt | 1 + .../utils/FakeWidgetMessageInterceptor.kt | 1 + features/call/test/build.gradle.kts | 34 +++ .../call/test/CallNotificationData.kt | 52 +++++ .../call/test/FakeElementCallEntryPoint.kt | 44 ++++ features/messages/impl/build.gradle.kts | 2 +- .../messages/impl/MessagesFlowNode.kt | 12 +- .../impl/timeline/TimelineControllerTest.kt | 1 + features/roomdetails/impl/build.gradle.kts | 2 +- .../roomdetails/impl/RoomDetailsFlowNode.kt | 12 +- .../createkey/CreateNewRecoveryKeyView.kt | 5 +- features/userprofile/impl/build.gradle.kts | 2 +- .../userprofile/impl/UserProfileFlowNode.kt | 10 +- .../components/avatar/AvatarSize.kt | 1 + .../libraries/matrix/api/room/MatrixRoom.kt | 5 + .../api/timeline/item/event/EventType.kt | 4 +- .../matrix/impl/room/RustMatrixRoom.kt | 4 + .../matrix/test/room/FakeMatrixRoom.kt | 5 + libraries/push/api/build.gradle.kts | 1 + .../notifications/NotificationBitmapLoader.kt | 38 ++++ .../notifications/NotificationIdProvider.kt | 35 ++- .../OnMissedCallNotificationHandler.kt | 35 +++ libraries/push/impl/build.gradle.kts | 2 + .../ActiveNotificationsProvider.kt | 10 +- .../DefaultNotifiableEventResolver.kt | 49 ++++- ....kt => DefaultNotificationBitmapLoader.kt} | 12 +- .../DefaultNotificationDrawerManager.kt | 8 +- .../DefaultOnMissedCallNotificationHandler.kt | 41 ++++ .../notifications/NotificationDataFactory.kt | 4 + .../notifications/NotificationRenderer.kt | 17 +- .../notifications/RoomGroupMessageCreator.kt | 1 + .../channels/NotificationChannels.kt | 102 ++++++--- .../factories/NotificationCreator.kt | 17 +- .../model/NotifiableMessageEvent.kt | 19 +- .../model/NotifiableRingingCallEvent.kt | 52 +++++ .../push/impl/push/DefaultPushHandler.kt | 32 ++- .../impl/src/main/res/values/localazy.xml | 2 + .../DefaultActiveNotificationsProviderTest.kt | 10 +- .../DefaultNotifiableEventResolverTest.kt | 112 +++++++++- .../DefaultNotificationDrawerManagerTest.kt | 7 +- ...aultOnMissedCallNotificationHandlerTest.kt | 79 +++++++ .../DefaultRoomGroupMessageCreatorTest.kt | 4 +- .../NotificationDataFactoryTest.kt | 3 +- .../NotificationIdProviderTest.kt | 3 +- .../notifications/NotificationRendererTest.kt | 6 +- .../channels/FakeNotificationChannels.kt | 35 +++ .../channels/NotificationChannelsTest.kt | 83 +++++++ .../DefaultNotificationCreatorTest.kt | 10 +- .../fake/FakeNotificationDataFactory.kt | 4 +- .../fake/FakeNotificationDisplayer.kt | 4 +- .../fixtures/NotifiableEventFixture.kt | 37 +++- .../push/impl/push/DefaultPushHandlerTest.kt | 61 ++++++ libraries/push/test/build.gradle.kts | 6 + .../test/notifications}/FakeImageLoader.kt | 2 +- .../notifications}/FakeImageLoaderHolder.kt | 4 +- .../FakeOnMissedCallNotificationHandler.kt | 34 +++ .../push/FakeNotificationBitmapLoader.kt | 35 +++ .../src/main/res/values/localazy.xml | 1 + .../tests/testutils/lambda/LambdaRecorder.kt | 7 + ...reenView-Day-0_1_null,NEXUS_5,1.0,en].png} | 0 ...enView-Night-0_2_null,NEXUS_5,1.0,en].png} | 0 ...allScreen-Day-1_2_null,NEXUS_5,1.0,en].png | 3 + ...lScreen-Night-1_3_null,NEXUS_5,1.0,en].png | 3 + ...ryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png | 4 +- ...KeyView-Night-0_2_null,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_10,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_11,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_12,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_13,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_14,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_15,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_16,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_17,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_18,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_19,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_20,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_21,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_22,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_23,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_24,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_25,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_26,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_27,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_28,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_29,NEXUS_5,1.0,en].png | 4 +- ...vatars_Avatar_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_30,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_31,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_32,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_33,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_34,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_35,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_36,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_37,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_38,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_39,NEXUS_5,1.0,en].png | 4 +- ...vatars_Avatar_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_40,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_41,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_42,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_43,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_44,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_45,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_46,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_47,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_48,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_49,NEXUS_5,1.0,en].png | 4 +- ...vatars_Avatar_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_50,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_51,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_52,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_53,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_54,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_55,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_56,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_57,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_58,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_59,NEXUS_5,1.0,en].png | 4 +- ...vatars_Avatar_0_null_6,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_60,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_61,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_62,NEXUS_5,1.0,en].png | 4 +- ...atars_Avatar_0_null_63,NEXUS_5,1.0,en].png | 3 + ...atars_Avatar_0_null_64,NEXUS_5,1.0,en].png | 3 + ...atars_Avatar_0_null_65,NEXUS_5,1.0,en].png | 3 + ...vatars_Avatar_0_null_7,NEXUS_5,1.0,en].png | 4 +- ...vatars_Avatar_0_null_8,NEXUS_5,1.0,en].png | 4 +- ...vatars_Avatar_0_null_9,NEXUS_5,1.0,en].png | 4 +- tools/localazy/config.json | 5 +- 186 files changed, 2686 insertions(+), 330 deletions(-) create mode 100644 changelog.d/2894.feature create mode 100644 features/call/api/build.gradle.kts rename features/call/{src/main/kotlin/io/element/android/features/call => api/src/main/kotlin/io/element/android/features/call/api}/CallType.kt (92%) create mode 100644 features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt rename features/call/{ => impl}/build.gradle.kts (78%) rename features/call/{ => impl}/src/main/AndroidManifest.xml (75%) create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl}/data/WidgetMessage.kt (96%) rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl}/di/CallBindings.kt (67%) create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl/services}/CallForegroundService.kt (69%) rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl}/ui/CallScreenEvents.kt (80%) rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl}/ui/CallScreenPresenter.kt (91%) rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl}/ui/CallScreenState.kt (94%) rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl}/ui/CallScreenView.kt (96%) rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl}/ui/ElementCallActivity.kt (86%) create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl}/utils/CallIntentDataParser.kt (98%) rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl}/utils/CallWidgetProvider.kt (95%) rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl}/utils/DefaultCallWidgetProvider.kt (97%) create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl}/utils/WebViewWidgetMessageInterceptor.kt (97%) rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl}/utils/WidgetMessageInterceptor.kt (93%) rename features/call/{src/main/kotlin/io/element/android/features/call => impl/src/main/kotlin/io/element/android/features/call/impl}/utils/WidgetMessageSerializer.kt (89%) rename features/call/{ => impl}/src/main/res/values-be/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-cs/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-de/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-es/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-fr/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-hu/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-in/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-it/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-ka/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-pt/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-ro/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-ru/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-sk/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-sv/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-uk/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-zh-rTW/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values-zh/translations.xml (100%) rename features/call/{ => impl}/src/main/res/values/do_not_translate.xml (100%) rename features/call/{ => impl}/src/main/res/values/localazy.xml (81%) create mode 100644 features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt rename features/call/{ => impl}/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt (95%) create mode 100644 features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt rename features/call/{ => impl}/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt (96%) rename features/call/{ => impl}/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt (92%) rename features/call/{ => impl}/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt (99%) create mode 100644 features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt rename features/call/{ => impl}/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt (98%) create mode 100644 features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt rename features/call/{ => impl}/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt (95%) rename features/call/{ => impl}/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt (93%) create mode 100644 features/call/test/build.gradle.kts create mode 100644 features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt create mode 100644 features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt create mode 100644 libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt rename libraries/push/{impl/src/main/kotlin/io/element/android/libraries/push/impl => api/src/main/kotlin/io/element/android/libraries/push/api}/notifications/NotificationIdProvider.kt (63%) create mode 100644 libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/OnMissedCallNotificationHandler.kt rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/{NotificationBitmapLoader.kt => DefaultNotificationBitmapLoader.kt} (86%) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/FakeNotificationChannels.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt rename libraries/push/{impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake => test/src/main/kotlin/io/element/android/libraries/push/test/notifications}/FakeImageLoader.kt (96%) rename libraries/push/{impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake => test/src/main/kotlin/io/element/android/libraries/push/test/notifications}/FakeImageLoaderHolder.kt (90%) create mode 100644 libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeOnMissedCallNotificationHandler.kt create mode 100644 libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.call.ui_CallScreenView_null_CallScreenView-Day-0_1_null,NEXUS_5,1.0,en].png => ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.call.ui_CallScreenView_null_CallScreenView-Night-0_2_null,NEXUS_5,1.0,en].png => ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Day-1_2_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Night-1_3_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_63,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_64,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_65,NEXUS_5,1.0,en].png diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt index bbd9f62689..64d75c6aa9 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt @@ -17,5 +17,13 @@ package io.element.android.appconfig object ElementCallConfig { + /** + * The default base URL for the Element Call service. + */ const val DEFAULT_BASE_URL = "https://call.element.io" + + /** + * The default duration of a ringing call in seconds before it's automatically dismissed. + */ + const val RINGING_CALL_DURATION_SECONDS = 15 } diff --git a/changelog.d/2894.feature b/changelog.d/2894.feature new file mode 100644 index 0000000000..6c9067ebed --- /dev/null +++ b/changelog.d/2894.feature @@ -0,0 +1 @@ +Ringing call notifications and full screen ringing screen for DMs when the device is locked. diff --git a/features/call/api/build.gradle.kts b/features/call/api/build.gradle.kts new file mode 100644 index 0000000000..100960d544 --- /dev/null +++ b/features/call/api/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.call.api" +} + +dependencies { + implementation(projects.anvilannotations) + implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + implementation(projects.libraries.matrix.api) +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallType.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt similarity index 92% rename from features/call/src/main/kotlin/io/element/android/features/call/CallType.kt rename to features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt index a3615671b6..4ce3a35fb6 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/CallType.kt +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2024 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call +package io.element.android.features.call.api import android.os.Parcelable import io.element.android.libraries.architecture.NodeInputs diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt new file mode 100644 index 0000000000..5f4a5c3a65 --- /dev/null +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.api + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId + +/** + * Entry point for the call feature. + */ +interface ElementCallEntryPoint { + /** + * Start a call of the given type. + * @param callType The type of call to start. + */ + fun startCall(callType: CallType) + + /** + * Handle an incoming call. + * @param callType The type of call. + * @param eventId The event id of the event that started the call. + * @param senderId The user id of the sender of the event that started the call. + * @param roomName The name of the room the call is in. + * @param senderName The name of the sender of the event that started the call. + * @param avatarUrl The avatar url of the room or DM. + * @param timestamp The timestamp of the event that started the call. + * @param notificationChannelId The id of the notification channel to use for the call notification. + */ + fun handleIncomingCall( + callType: CallType.RoomCall, + eventId: EventId, + senderId: UserId, + roomName: String?, + senderName: String?, + avatarUrl: String?, + timestamp: Long, + notificationChannelId: String, + ) +} diff --git a/features/call/build.gradle.kts b/features/call/impl/build.gradle.kts similarity index 78% rename from features/call/build.gradle.kts rename to features/call/impl/build.gradle.kts index 7ff1510cef..72ccdc089b 100644 --- a/features/call/build.gradle.kts +++ b/features/call/impl/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2024 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,11 +23,15 @@ plugins { } android { - namespace = "io.element.android.features.call" + namespace = "io.element.android.features.call.impl" buildFeatures { buildConfig = true } + + testOptions { + unitTests.isIncludeAndroidResources = true + } } anvil { @@ -41,12 +45,17 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.designsystem) implementation(projects.libraries.matrix.impl) + implementation(projects.libraries.matrixui) implementation(projects.libraries.network) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.push.api) + implementation(projects.libraries.uiStrings) implementation(projects.services.analytics.api) implementation(projects.services.toolbox.api) implementation(libs.androidx.webkit) + implementation(libs.coil.compose) implementation(libs.serialization.json) + api(projects.features.call.api) ksp(libs.showkase.processor) testImplementation(libs.coroutines.test) @@ -54,9 +63,12 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(libs.test.robolectric) + testImplementation(libs.test.mockk) + testImplementation(projects.features.call.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.push.test) testImplementation(projects.services.analytics.test) testImplementation(projects.tests.testutils) } diff --git a/features/call/src/main/AndroidManifest.xml b/features/call/impl/src/main/AndroidManifest.xml similarity index 75% rename from features/call/src/main/AndroidManifest.xml rename to features/call/impl/src/main/AndroidManifest.xml index 532d5bc40c..354ea7533d 100644 --- a/features/call/src/main/AndroidManifest.xml +++ b/features/call/impl/src/main/AndroidManifest.xml @@ -23,11 +23,17 @@ android:name="android.hardware.microphone" android:required="false" /> + + + + + + + + + + android:exported="false" + android:foregroundServiceType="phoneCall" /> + + + diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt new file mode 100644 index 0000000000..8350e01e8d --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.impl + +import android.content.Context +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.ElementCallEntryPoint +import io.element.android.features.call.impl.notifications.CallNotificationData +import io.element.android.features.call.impl.utils.ActiveCallManager +import io.element.android.features.call.impl.utils.IntentProvider +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultElementCallEntryPoint @Inject constructor( + @ApplicationContext private val context: Context, + private val activeCallManager: ActiveCallManager, +) : ElementCallEntryPoint { + companion object { + const val EXTRA_CALL_TYPE = "EXTRA_CALL_TYPE" + const val REQUEST_CODE = 2255 + } + + override fun startCall(callType: CallType) { + context.startActivity(IntentProvider.createIntent(context, callType)) + } + + override fun handleIncomingCall( + callType: CallType.RoomCall, + eventId: EventId, + senderId: UserId, + roomName: String?, + senderName: String?, + avatarUrl: String?, + timestamp: Long, + notificationChannelId: String, + ) { + val incomingCallNotificationData = CallNotificationData( + sessionId = callType.sessionId, + roomId = callType.roomId, + eventId = eventId, + senderId = senderId, + roomName = roomName, + senderName = senderName, + avatarUrl = avatarUrl, + timestamp = timestamp, + notificationChannelId = notificationChannelId, + ) + activeCallManager.registerIncomingCall(notificationData = incomingCallNotificationData) + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/data/WidgetMessage.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt similarity index 96% rename from features/call/src/main/kotlin/io/element/android/features/call/data/WidgetMessage.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt index 0d9be99cf7..a75132abae 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/data/WidgetMessage.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call.data +package io.element.android.features.call.impl.data import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/features/call/src/main/kotlin/io/element/android/features/call/di/CallBindings.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt similarity index 67% rename from features/call/src/main/kotlin/io/element/android/features/call/di/CallBindings.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt index acbf3dba1f..88fea81149 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/di/CallBindings.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt @@ -14,13 +14,17 @@ * limitations under the License. */ -package io.element.android.features.call.di +package io.element.android.features.call.impl.di import com.squareup.anvil.annotations.ContributesTo -import io.element.android.features.call.ui.ElementCallActivity +import io.element.android.features.call.impl.receivers.DeclineCallBroadcastReceiver +import io.element.android.features.call.impl.ui.ElementCallActivity +import io.element.android.features.call.impl.ui.IncomingCallActivity import io.element.android.libraries.di.AppScope @ContributesTo(AppScope::class) interface CallBindings { fun inject(callActivity: ElementCallActivity) + fun inject(callActivity: IncomingCallActivity) + fun inject(declineCallBroadcastReceiver: DeclineCallBroadcastReceiver) } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt new file mode 100644 index 0000000000..9f49c6e869 --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.impl.notifications + +import android.os.Parcelable +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.parcelize.Parcelize + +@Parcelize +data class CallNotificationData( + val sessionId: SessionId, + val roomId: RoomId, + val eventId: EventId, + val senderId: UserId, + val roomName: String?, + val senderName: String?, + val avatarUrl: String?, + val notificationChannelId: String, + val timestamp: Long, +) : Parcelable diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt new file mode 100644 index 0000000000..cd4c3692d2 --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.element.android.features.call.impl.notifications + +import android.app.Notification +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.media.AudioManager +import android.media.RingtoneManager +import androidx.core.app.NotificationCompat +import androidx.core.app.PendingIntentCompat +import androidx.core.app.Person +import io.element.android.appconfig.ElementCallConfig +import io.element.android.features.call.api.CallType +import io.element.android.features.call.impl.receivers.DeclineCallBroadcastReceiver +import io.element.android.features.call.impl.ui.IncomingCallActivity +import io.element.android.features.call.impl.utils.IntentProvider +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder +import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds + +/** + * Creates a notification for a ringing call. + */ +class RingingCallNotificationCreator @Inject constructor( + @ApplicationContext private val context: Context, + private val matrixClientProvider: MatrixClientProvider, + private val imageLoaderHolder: ImageLoaderHolder, + private val notificationBitmapLoader: NotificationBitmapLoader, +) { + companion object { + /** + * Request code for the decline action. + */ + const val DECLINE_REQUEST_CODE = 1 + + /** + * Request code for the full screen intent. + */ + const val FULL_SCREEN_INTENT_REQUEST_CODE = 2 + } + + suspend fun createNotification( + sessionId: SessionId, + roomId: RoomId, + eventId: EventId, + senderId: UserId, + roomName: String?, + senderDisplayName: String, + roomAvatarUrl: String?, + notificationChannelId: String, + timestamp: Long, + ): Notification? { + val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null + val imageLoader = imageLoaderHolder.get(matrixClient) + val largeIcon = notificationBitmapLoader.getUserIcon(roomAvatarUrl, imageLoader) + + val caller = Person.Builder() + .setName(senderDisplayName) + .setIcon(largeIcon) + .setImportant(true) + .build() + + val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId)) + + val declineIntent = PendingIntentCompat.getBroadcast( + context, + DECLINE_REQUEST_CODE, + Intent(context, DeclineCallBroadcastReceiver::class.java), + PendingIntent.FLAG_CANCEL_CURRENT, + false, + )!! + + val fullScreenIntent = PendingIntentCompat.getActivity( + context, + FULL_SCREEN_INTENT_REQUEST_CODE, + Intent(context, IncomingCallActivity::class.java).apply { + putExtra( + IncomingCallActivity.EXTRA_NOTIFICATION_DATA, + CallNotificationData(sessionId, roomId, eventId, senderId, roomName, senderDisplayName, roomAvatarUrl, notificationChannelId, timestamp) + ) + }, + PendingIntent.FLAG_CANCEL_CURRENT, + false + ) + + val ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE) + return NotificationCompat.Builder(context, notificationChannelId) + .setSmallIcon(CommonDrawables.ic_notification_small) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setCategory(NotificationCompat.CATEGORY_CALL) + .setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, declineIntent, answerIntent).setIsVideo(true)) + .addPerson(caller) + .setAutoCancel(true) + .setWhen(timestamp) + .setOngoing(true) + .setShowWhen(false) + .setSound(ringtoneUri, AudioManager.STREAM_RING) + .setTimeoutAfter(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds.inWholeMilliseconds) + .setContentIntent(answerIntent) + .setDeleteIntent(declineIntent) + .setFullScreenIntent(fullScreenIntent, true) + .build() + .apply { + flags = flags.or(Notification.FLAG_INSISTENT) + } + } +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt new file mode 100644 index 0000000000..27b46db5fe --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.impl.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import io.element.android.features.call.impl.di.CallBindings +import io.element.android.features.call.impl.utils.ActiveCallManager +import io.element.android.libraries.architecture.bindings +import javax.inject.Inject + +/** + * Broadcast receiver to decline the incoming call. + */ +class DeclineCallBroadcastReceiver : BroadcastReceiver() { + @Inject + lateinit var activeCallManager: ActiveCallManager + override fun onReceive(context: Context, intent: Intent?) { + context.bindings().inject(this) + activeCallManager.hungUpCall() + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt similarity index 69% rename from features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt index 10168eba2f..29f7f92139 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2024 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,30 +14,36 @@ * limitations under the License. */ -package io.element.android.features.call +package io.element.android.features.call.impl.services import android.app.Service import android.content.Context import android.content.Intent +import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.PendingIntentCompat +import androidx.core.app.ServiceCompat +import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.IconCompat -import io.element.android.features.call.ui.ElementCallActivity +import io.element.android.features.call.impl.R +import io.element.android.features.call.impl.ui.ElementCallActivity import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.push.api.notifications.ForegroundServiceType +import io.element.android.libraries.push.api.notifications.NotificationIdProvider +import timber.log.Timber +/** + * A foreground service that shows a notification for an ongoing call while the UI is in background. + */ class CallForegroundService : Service() { companion object { fun start(context: Context) { val intent = Intent(context, CallForegroundService::class.java) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(intent) - } else { - context.startService(intent) - } + ContextCompat.startForegroundService(context, intent) } fun stop(context: Context) { @@ -69,7 +75,17 @@ class CallForegroundService : Service() { .setContentText(getString(R.string.call_foreground_service_message_android)) .setContentIntent(pendingIntent) .build() - startForeground(1, notification) + val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.ONGOING_CALL) + val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL + } else { + 0 + } + runCatching { + ServiceCompat.startForeground(this, notificationId, notification, serviceType) + }.onFailure { + Timber.e(it, "Failed to start ongoing call foreground service") + } } override fun onDestroy() { diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt similarity index 80% rename from features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt index d16baacf3e..a4d60549c9 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt @@ -14,11 +14,12 @@ * limitations under the License. */ -package io.element.android.features.call.ui +package io.element.android.features.call.impl.ui -import io.element.android.features.call.utils.WidgetMessageInterceptor +import io.element.android.features.call.impl.utils.WidgetMessageInterceptor sealed interface CallScreenEvents { data object Hangup : CallScreenEvents - data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents + data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : + CallScreenEvents } diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt similarity index 91% rename from features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt index 49f6352212..a2e8359284 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call.ui +package io.element.android.features.call.impl.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -30,11 +30,12 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.features.call.CallType -import io.element.android.features.call.data.WidgetMessage -import io.element.android.features.call.utils.CallWidgetProvider -import io.element.android.features.call.utils.WidgetMessageInterceptor -import io.element.android.features.call.utils.WidgetMessageSerializer +import io.element.android.features.call.api.CallType +import io.element.android.features.call.impl.data.WidgetMessage +import io.element.android.features.call.impl.utils.ActiveCallManager +import io.element.android.features.call.impl.utils.CallWidgetProvider +import io.element.android.features.call.impl.utils.WidgetMessageInterceptor +import io.element.android.features.call.impl.utils.WidgetMessageSerializer import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState @@ -65,6 +66,7 @@ class CallScreenPresenter @AssistedInject constructor( private val matrixClientsProvider: MatrixClientProvider, private val screenTracker: ScreenTracker, private val appCoroutineScope: CoroutineScope, + private val activeCallManager: ActiveCallManager, ) : Presenter { @AssistedFactory interface Factory { @@ -84,6 +86,10 @@ class CallScreenPresenter @AssistedInject constructor( LaunchedEffect(Unit) { loadUrl(callType, urlState, callWidgetDriver) + + if (callType is CallType.RoomCall) { + activeCallManager.joinedCall(callType.sessionId, callType.roomId) + } } when (callType) { @@ -134,6 +140,14 @@ class CallScreenPresenter @AssistedInject constructor( } } + DisposableEffect(Unit) { + onDispose { + if (callType is CallType.RoomCall) { + activeCallManager.hungUpCall() + } + } + } + fun handleEvents(event: CallScreenEvents) { when (event) { is CallScreenEvents.Hangup -> { @@ -193,7 +207,6 @@ class CallScreenPresenter @AssistedInject constructor( val client = (callType as? CallType.RoomCall)?.sessionId?.let { matrixClientsProvider.getOrNull(it) } ?: return@DisposableEffect onDispose { } - coroutineScope.launch { client.syncService().syncState .onEach { state -> diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt similarity index 94% rename from features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt index 76926bfb9f..9b0931d5fd 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call.ui +package io.element.android.features.call.impl.ui import io.element.android.libraries.architecture.AsyncData diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt similarity index 96% rename from features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt index cc62ce03e1..ebf006d102 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call.ui +package io.element.android.features.call.impl.ui import android.annotation.SuppressLint import android.view.ViewGroup @@ -34,8 +34,8 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.call.R -import io.element.android.features.call.utils.WebViewWidgetMessageInterceptor +import io.element.android.features.call.impl.R +import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt similarity index 86% rename from features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt index f810dbb496..8d6324c86a 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt @@ -14,12 +14,10 @@ * limitations under the License. */ -package io.element.android.features.call.ui +package io.element.android.features.call.impl.ui import android.Manifest -import android.content.Context import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.res.Configuration import android.media.AudioAttributes import android.media.AudioFocusRequest @@ -41,30 +39,16 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.Theme import io.element.android.compound.theme.isDark import io.element.android.compound.theme.mapToTheme -import io.element.android.features.call.CallForegroundService -import io.element.android.features.call.CallType -import io.element.android.features.call.di.CallBindings -import io.element.android.features.call.utils.CallIntentDataParser +import io.element.android.features.call.api.CallType +import io.element.android.features.call.impl.DefaultElementCallEntryPoint +import io.element.android.features.call.impl.di.CallBindings +import io.element.android.features.call.impl.services.CallForegroundService +import io.element.android.features.call.impl.utils.CallIntentDataParser import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.libraries.architecture.bindings import javax.inject.Inject class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { - companion object { - private const val EXTRA_CALL_WIDGET_SETTINGS = "EXTRA_CALL_WIDGET_SETTINGS" - - fun start( - context: Context, - callInputs: CallType, - ) { - val intent = Intent(context, ElementCallActivity::class.java).apply { - putExtra(EXTRA_CALL_WIDGET_SETTINGS, callInputs) - addFlags(FLAG_ACTIVITY_NEW_TASK) - } - context.startActivity(intent) - } - } - @Inject lateinit var callIntentDataParser: CallIntentDataParser @Inject lateinit var presenterFactory: CallScreenPresenter.Factory @Inject lateinit var appPreferencesStore: AppPreferencesStore @@ -88,7 +72,13 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { applicationContext.bindings().inject(this) - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + @Suppress("DEPRECATION") + window.addFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or + WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON or + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or + WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + ) setCallType(intent) @@ -157,16 +147,16 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { } private fun setCallType(intent: Intent?) { - val inputs = intent?.let { - IntentCompat.getParcelableExtra(it, EXTRA_CALL_WIDGET_SETTINGS, CallType::class.java) + val callType = intent?.let { + IntentCompat.getParcelableExtra(it, DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, CallType::class.java) } val intentUrl = intent?.dataString?.let(::parseUrl) when { // Re-opened the activity but we have no url to load or a cached one, finish the activity - intent?.dataString == null && inputs == null && webViewTarget.value == null -> finish() - inputs != null -> { - webViewTarget.value = inputs - presenter = presenterFactory.create(inputs, this) + intent?.dataString == null && callType == null && webViewTarget.value == null -> finish() + callType != null -> { + webViewTarget.value = callType + presenter = presenterFactory.create(callType, this) } intentUrl != null -> { val fallbackInputs = CallType.ExternalUrl(intentUrl) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt new file mode 100644 index 0000000000..cc39dd124f --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.impl.ui + +import android.os.Bundle +import android.view.WindowManager +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.IntentCompat +import androidx.lifecycle.lifecycleScope +import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.ElementCallEntryPoint +import io.element.android.features.call.impl.di.CallBindings +import io.element.android.features.call.impl.notifications.CallNotificationData +import io.element.android.features.call.impl.utils.ActiveCallManager +import io.element.android.features.call.impl.utils.CallState +import io.element.android.libraries.architecture.bindings +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +/** + * Activity that's displayed as a full screen intent when an incoming call is received. + */ +class IncomingCallActivity : AppCompatActivity() { + companion object { + /** + * Extra key for the notification data. + */ + const val EXTRA_NOTIFICATION_DATA = "EXTRA_NOTIFICATION_DATA" + } + + @Inject + lateinit var elementCallEntryPoint: ElementCallEntryPoint + + @Inject + lateinit var activeCallManager: ActiveCallManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + applicationContext.bindings().inject(this) + + // Set flags so it can be displayed in the lock screen + @Suppress("DEPRECATION") + window.addFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or + WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON or + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or + WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + ) + + val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) } + if (notificationData != null) { + setContent { + IncomingCallScreen( + notificationData = notificationData, + onAnswer = ::onAnswer, + onCancel = ::onCancel, + ) + } + } else { + // No data, finish the activity + finish() + return + } + + activeCallManager.activeCall + .filter { it?.callState !is CallState.Ringing } + .onEach { finish() } + .launchIn(lifecycleScope) + } + + private fun onAnswer(notificationData: CallNotificationData) { + elementCallEntryPoint.startCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId)) + } + + private fun onCancel() { + activeCallManager.hungUpCall() + } +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt new file mode 100644 index 0000000000..80dc2353ca --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.impl.ui + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.call.impl.R +import io.element.android.features.call.impl.notifications.CallNotificationData +import io.element.android.libraries.designsystem.background.OnboardingBackground +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +internal fun IncomingCallScreen( + notificationData: CallNotificationData, + onAnswer: (CallNotificationData) -> Unit, + onCancel: () -> Unit, +) { + ElementTheme { + OnboardingBackground() + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Bottom + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp, top = 124.dp) + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Avatar( + avatarData = AvatarData( + id = notificationData.senderId.value, + name = notificationData.senderName, + url = notificationData.avatarUrl, + size = AvatarSize.IncomingCall, + ) + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = notificationData.senderName ?: notificationData.senderId.value, + style = ElementTheme.typography.fontHeadingMdBold, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.screen_incoming_call_subtitle_android), + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 24.dp, end = 24.dp, bottom = 64.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + ActionButton( + size = 64.dp, + onClick = { onAnswer(notificationData) }, + icon = CompoundIcons.VoiceCall(), + title = stringResource(CommonStrings.action_accept), + backgroundColor = ElementTheme.colors.iconSuccessPrimary, + borderColor = ElementTheme.colors.borderSuccessSubtle + ) + + ActionButton( + size = 64.dp, + onClick = onCancel, + icon = CompoundIcons.EndCall(), + title = stringResource(CommonStrings.action_reject), + backgroundColor = ElementTheme.colors.iconCriticalPrimary, + borderColor = ElementTheme.colors.borderCriticalSubtle + ) + } + } + } +} + +@Composable +private fun ActionButton( + size: Dp, + onClick: () -> Unit, + icon: ImageVector, + title: String, + backgroundColor: Color, + borderColor: Color, + contentDescription: String? = title, + borderSize: Dp = 1.33.dp, +) { + Column( + modifier = Modifier.width(120.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + FilledIconButton( + modifier = Modifier.size(size + borderSize) + .border(borderSize, borderColor, CircleShape), + onClick = onClick, + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = backgroundColor, + contentColor = Color.White, + ) + ) { + Icon( + modifier = Modifier.size(32.dp), + imageVector = icon, + contentDescription = contentDescription + ) + } + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = title, + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun IncomingCallScreenPreview() { + ElementPreview { + IncomingCallScreen( + notificationData = CallNotificationData( + sessionId = SessionId("@alice:matrix.org"), + roomId = RoomId("!1234:matrix.org"), + eventId = EventId("\$asdadadsad:matrix.org"), + senderId = UserId("@bob:matrix.org"), + roomName = "A room", + senderName = "Bob", + avatarUrl = null, + notificationChannelId = "incoming_call", + timestamp = 0L, + ), + onAnswer = {}, + onCancel = {}, + ) + } +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt new file mode 100644 index 0000000000..7f2f601bd6 --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.impl.utils + +import android.annotation.SuppressLint +import androidx.core.app.NotificationManagerCompat +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.ElementCallConfig +import io.element.android.features.call.impl.notifications.CallNotificationData +import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.push.api.notifications.ForegroundServiceType +import io.element.android.libraries.push.api.notifications.NotificationIdProvider +import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds + +/** + * Manages the active call state. + */ +interface ActiveCallManager { + /** + * The active call state flow, which will be updated when the active call changes. + */ + val activeCall: StateFlow + + /** + * Registers an incoming call if there isn't an existing active call and posts a [CallState.Ringing] notification. + * @param notificationData The data for the incoming call notification. + */ + fun registerIncomingCall(notificationData: CallNotificationData) + + /** + * Called when the incoming call timed out. It will remove the active call and remove any associated UI, adding a 'missed call' notification. + */ + fun incomingCallTimedOut() + + /** + * Hangs up the active call and removes any associated UI. + */ + fun hungUpCall() + + /** + * Called when the user joins a call. It will remove any existing UI and set the call state as [CallState.InCall]. + * + * @param sessionId The session ID of the user joining the call. + * @param roomId The room ID of the call. + */ + fun joinedCall(sessionId: SessionId, roomId: RoomId) +} + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultActiveCallManager @Inject constructor( + private val coroutineScope: CoroutineScope, + private val matrixClientProvider: MatrixClientProvider, + private val onMissedCallNotificationHandler: OnMissedCallNotificationHandler, + private val ringingCallNotificationCreator: RingingCallNotificationCreator, + private val notificationManagerCompat: NotificationManagerCompat, +) : ActiveCallManager { + private var timedOutCallJob: Job? = null + + override val activeCall = MutableStateFlow(null) + + override fun registerIncomingCall(notificationData: CallNotificationData) { + if (activeCall.value != null) { + Timber.w("Already have an active call, ignoring incoming call: $notificationData") + return + } + activeCall.value = ActiveCall( + sessionId = notificationData.sessionId, + roomId = notificationData.roomId, + callState = CallState.Ringing(notificationData), + ) + + timedOutCallJob = coroutineScope.launch { + registerIncomingCall(notificationData) + showIncomingCallNotification(notificationData) + + // Wait for the call to end + delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds) + incomingCallTimedOut() + } + } + + override fun incomingCallTimedOut() { + val previousActiveCall = activeCall.value ?: return + val notificationData = (previousActiveCall.callState as? CallState.Ringing)?.notificationData ?: return + activeCall.value = null + + cancelIncomingCallNotification() + + coroutineScope.launch { + onMissedCallNotificationHandler.addMissedCallNotification( + sessionId = previousActiveCall.sessionId, + roomId = previousActiveCall.roomId, + eventId = notificationData.eventId, + ) + } + } + + override fun hungUpCall() { + cancelIncomingCallNotification() + timedOutCallJob?.cancel() + activeCall.value = null + } + + override fun joinedCall(sessionId: SessionId, roomId: RoomId) { + cancelIncomingCallNotification() + timedOutCallJob?.cancel() + + activeCall.value = ActiveCall( + sessionId = sessionId, + roomId = roomId, + callState = CallState.InCall, + ) + // Send call notification to the room + coroutineScope.launch { + matrixClientProvider.getOrRestore(sessionId) + .getOrNull() + ?.getRoom(roomId) + ?.sendCallNotificationIfNeeded() + } + } + + @SuppressLint("MissingPermission") + private suspend fun showIncomingCallNotification(notificationData: CallNotificationData) { + val notification = ringingCallNotificationCreator.createNotification( + sessionId = notificationData.sessionId, + roomId = notificationData.roomId, + eventId = notificationData.eventId, + senderId = notificationData.senderId, + roomName = notificationData.roomName, + senderDisplayName = notificationData.senderName ?: notificationData.senderId.value, + roomAvatarUrl = notificationData.avatarUrl, + notificationChannelId = notificationData.notificationChannelId, + timestamp = notificationData.timestamp + ) ?: return + runCatching { + notificationManagerCompat.notify( + NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.INCOMING_CALL), + notification, + ) + }.onFailure { + Timber.e(it, "Failed to publish notification for incoming call") + } + } + + private fun cancelIncomingCallNotification() { + notificationManagerCompat.cancel(NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.INCOMING_CALL)) + } +} + +/** + * Represents an active call. + */ +data class ActiveCall( + val sessionId: SessionId, + val roomId: RoomId, + val callState: CallState, +) + +/** + * Represents the state of an active call. + */ +sealed interface CallState { + /** + * The call is in a ringing state. + * @param notificationData The data for the incoming call notification. + */ + data class Ringing(val notificationData: CallNotificationData) : CallState + + /** + * The call is in an in-call state. + */ + data object InCall : CallState +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/CallIntentDataParser.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt similarity index 98% rename from features/call/src/main/kotlin/io/element/android/features/call/utils/CallIntentDataParser.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt index c9f4532951..fcbd535a37 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/utils/CallIntentDataParser.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call.utils +package io.element.android.features.call.impl.utils import android.net.Uri import javax.inject.Inject diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt similarity index 95% rename from features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt index b65298854d..670571476c 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call.utils +package io.element.android.features.call.impl.utils import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt similarity index 97% rename from features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt index 7fb6d3cb48..1daa0a8f3d 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call.utils +package io.element.android.features.call.impl.utils import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.ElementCallConfig diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt new file mode 100644 index 0000000000..bc5816220a --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.impl.utils + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.PendingIntentCompat +import io.element.android.features.call.api.CallType +import io.element.android.features.call.impl.DefaultElementCallEntryPoint +import io.element.android.features.call.impl.ui.ElementCallActivity + +internal object IntentProvider { + fun createIntent(context: Context, callType: CallType): Intent = Intent(context, ElementCallActivity::class.java).apply { + putExtra(DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, callType) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION) + } + + fun getPendingIntent(context: Context, callType: CallType): PendingIntent { + return PendingIntentCompat.getActivity( + context, + DefaultElementCallEntryPoint.REQUEST_CODE, + createIntent(context, callType), + 0, + false + )!! + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt similarity index 97% rename from features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt index c4676ac9dc..5332c75f6c 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call.utils +package io.element.android.features.call.impl.utils import android.graphics.Bitmap import android.webkit.JavascriptInterface @@ -22,7 +22,7 @@ import android.webkit.WebView import android.webkit.WebViewClient import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature -import io.element.android.features.call.BuildConfig +import io.element.android.features.call.impl.BuildConfig import kotlinx.coroutines.flow.MutableSharedFlow class WebViewWidgetMessageInterceptor( diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageInterceptor.kt similarity index 93% rename from features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageInterceptor.kt index fa5c3bea67..6a6ca5788a 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageInterceptor.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call.utils +package io.element.android.features.call.impl.utils import kotlinx.coroutines.flow.Flow diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt similarity index 89% rename from features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt index aa7424e9a3..3d97a31ceb 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.call.utils +package io.element.android.features.call.impl.utils -import io.element.android.features.call.data.WidgetMessage +import io.element.android.features.call.impl.data.WidgetMessage import kotlinx.serialization.json.Json object WidgetMessageSerializer { diff --git a/features/call/src/main/res/values-be/translations.xml b/features/call/impl/src/main/res/values-be/translations.xml similarity index 100% rename from features/call/src/main/res/values-be/translations.xml rename to features/call/impl/src/main/res/values-be/translations.xml diff --git a/features/call/src/main/res/values-cs/translations.xml b/features/call/impl/src/main/res/values-cs/translations.xml similarity index 100% rename from features/call/src/main/res/values-cs/translations.xml rename to features/call/impl/src/main/res/values-cs/translations.xml diff --git a/features/call/src/main/res/values-de/translations.xml b/features/call/impl/src/main/res/values-de/translations.xml similarity index 100% rename from features/call/src/main/res/values-de/translations.xml rename to features/call/impl/src/main/res/values-de/translations.xml diff --git a/features/call/src/main/res/values-es/translations.xml b/features/call/impl/src/main/res/values-es/translations.xml similarity index 100% rename from features/call/src/main/res/values-es/translations.xml rename to features/call/impl/src/main/res/values-es/translations.xml diff --git a/features/call/src/main/res/values-fr/translations.xml b/features/call/impl/src/main/res/values-fr/translations.xml similarity index 100% rename from features/call/src/main/res/values-fr/translations.xml rename to features/call/impl/src/main/res/values-fr/translations.xml diff --git a/features/call/src/main/res/values-hu/translations.xml b/features/call/impl/src/main/res/values-hu/translations.xml similarity index 100% rename from features/call/src/main/res/values-hu/translations.xml rename to features/call/impl/src/main/res/values-hu/translations.xml diff --git a/features/call/src/main/res/values-in/translations.xml b/features/call/impl/src/main/res/values-in/translations.xml similarity index 100% rename from features/call/src/main/res/values-in/translations.xml rename to features/call/impl/src/main/res/values-in/translations.xml diff --git a/features/call/src/main/res/values-it/translations.xml b/features/call/impl/src/main/res/values-it/translations.xml similarity index 100% rename from features/call/src/main/res/values-it/translations.xml rename to features/call/impl/src/main/res/values-it/translations.xml diff --git a/features/call/src/main/res/values-ka/translations.xml b/features/call/impl/src/main/res/values-ka/translations.xml similarity index 100% rename from features/call/src/main/res/values-ka/translations.xml rename to features/call/impl/src/main/res/values-ka/translations.xml diff --git a/features/call/src/main/res/values-pt/translations.xml b/features/call/impl/src/main/res/values-pt/translations.xml similarity index 100% rename from features/call/src/main/res/values-pt/translations.xml rename to features/call/impl/src/main/res/values-pt/translations.xml diff --git a/features/call/src/main/res/values-ro/translations.xml b/features/call/impl/src/main/res/values-ro/translations.xml similarity index 100% rename from features/call/src/main/res/values-ro/translations.xml rename to features/call/impl/src/main/res/values-ro/translations.xml diff --git a/features/call/src/main/res/values-ru/translations.xml b/features/call/impl/src/main/res/values-ru/translations.xml similarity index 100% rename from features/call/src/main/res/values-ru/translations.xml rename to features/call/impl/src/main/res/values-ru/translations.xml diff --git a/features/call/src/main/res/values-sk/translations.xml b/features/call/impl/src/main/res/values-sk/translations.xml similarity index 100% rename from features/call/src/main/res/values-sk/translations.xml rename to features/call/impl/src/main/res/values-sk/translations.xml diff --git a/features/call/src/main/res/values-sv/translations.xml b/features/call/impl/src/main/res/values-sv/translations.xml similarity index 100% rename from features/call/src/main/res/values-sv/translations.xml rename to features/call/impl/src/main/res/values-sv/translations.xml diff --git a/features/call/src/main/res/values-uk/translations.xml b/features/call/impl/src/main/res/values-uk/translations.xml similarity index 100% rename from features/call/src/main/res/values-uk/translations.xml rename to features/call/impl/src/main/res/values-uk/translations.xml diff --git a/features/call/src/main/res/values-zh-rTW/translations.xml b/features/call/impl/src/main/res/values-zh-rTW/translations.xml similarity index 100% rename from features/call/src/main/res/values-zh-rTW/translations.xml rename to features/call/impl/src/main/res/values-zh-rTW/translations.xml diff --git a/features/call/src/main/res/values-zh/translations.xml b/features/call/impl/src/main/res/values-zh/translations.xml similarity index 100% rename from features/call/src/main/res/values-zh/translations.xml rename to features/call/impl/src/main/res/values-zh/translations.xml diff --git a/features/call/src/main/res/values/do_not_translate.xml b/features/call/impl/src/main/res/values/do_not_translate.xml similarity index 100% rename from features/call/src/main/res/values/do_not_translate.xml rename to features/call/impl/src/main/res/values/do_not_translate.xml diff --git a/features/call/src/main/res/values/localazy.xml b/features/call/impl/src/main/res/values/localazy.xml similarity index 81% rename from features/call/src/main/res/values/localazy.xml rename to features/call/impl/src/main/res/values/localazy.xml index cfe40526f4..5a386b2416 100644 --- a/features/call/src/main/res/values/localazy.xml +++ b/features/call/impl/src/main/res/values/localazy.xml @@ -3,4 +3,5 @@ "Ongoing call" "Tap to return to the call" "☎️ Call in progress" + "Incoming Element Call" diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt new file mode 100644 index 0000000000..c78b0bbe20 --- /dev/null +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call + +import android.content.Intent +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.api.CallType +import io.element.android.features.call.impl.DefaultElementCallEntryPoint +import io.element.android.features.call.impl.notifications.CallNotificationData +import io.element.android.features.call.impl.ui.ElementCallActivity +import io.element.android.features.call.utils.FakeActiveCallManager +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.tests.testutils.lambda.lambdaRecorder +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows.shadowOf + +@RunWith(RobolectricTestRunner::class) +class DefaultElementCallEntryPointTest { + @Test + fun `startCall - starts ElementCallActivity setup with the needed extras`() { + val entryPoint = createEntryPoint() + entryPoint.startCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID)) + + val expectedIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, ElementCallActivity::class.java) + val intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity + assertThat(intent.component).isEqualTo(expectedIntent.component) + assertThat(intent.extras?.containsKey("EXTRA_CALL_TYPE")).isTrue() + } + + @Test + fun `handleIncomingCall - registers the incoming call using ActiveCallManager`() { + val registerIncomingCallLambda = lambdaRecorder {} + val activeCallManager = FakeActiveCallManager(registerIncomingCallResult = registerIncomingCallLambda) + val entryPoint = createEntryPoint(activeCallManager = activeCallManager) + + entryPoint.handleIncomingCall( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + eventId = AN_EVENT_ID, + senderId = A_USER_ID_2, + roomName = "roomName", + senderName = "senderName", + avatarUrl = "avatarUrl", + timestamp = 0, + notificationChannelId = "notificationChannelId", + ) + + registerIncomingCallLambda.assertions().isCalledOnce() + } + + private fun createEntryPoint( + activeCallManager: FakeActiveCallManager = FakeActiveCallManager(), + ) = DefaultElementCallEntryPoint( + context = InstrumentationRegistry.getInstrumentation().targetContext, + activeCallManager = activeCallManager, + ) +} diff --git a/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt similarity index 95% rename from features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt rename to features/call/impl/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt index 6a24698efa..359483520a 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt @@ -19,7 +19,7 @@ package io.element.android.features.call import android.Manifest import android.webkit.PermissionRequest import com.google.common.truth.Truth.assertThat -import io.element.android.features.call.ui.mapWebkitPermissions +import io.element.android.features.call.impl.ui.mapWebkitPermissions import org.junit.Test class MapWebkitPermissionsTest { diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt new file mode 100644 index 0000000000..40cdc0522f --- /dev/null +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.notifications + +import androidx.core.graphics.drawable.IconCompat +import androidx.test.platform.app.InstrumentationRegistry +import coil.ImageLoader +import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.FakeMatrixClientProvider +import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder +import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class RingingCallNotificationCreatorTest { + @Test + fun `createNotification - with no associated MatrixClient does nothing`() = runTest { + val notificationCreator = createRingingCallNotificationCreator( + matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(IllegalStateException("No client found")) }) + ) + + val result = notificationCreator.createTestNotification() + + assertThat(result).isNull() + } + + @Test + fun `createNotification - creates a valid notification`() = runTest { + val notificationCreator = createRingingCallNotificationCreator( + matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(FakeMatrixClient()) }) + ) + + val result = notificationCreator.createTestNotification() + + assertThat(result).isNotNull() + } + + @Test + fun `createNotification - tries to load the avatar URL`() = runTest { + val getUserIconLambda = lambdaRecorder { _, _ -> null } + val notificationCreator = createRingingCallNotificationCreator( + matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(FakeMatrixClient()) }), + notificationBitmapLoader = FakeNotificationBitmapLoader(getUserIconResult = getUserIconLambda) + ) + + notificationCreator.createTestNotification() + + getUserIconLambda.assertions().isCalledOnce() + } + + private suspend fun RingingCallNotificationCreator.createTestNotification() = createNotification( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + senderId = A_USER_ID_2, + roomName = "Room", + senderDisplayName = "Johnnie Murphy", + roomAvatarUrl = "https://example.com/avatar.jpg", + notificationChannelId = "channelId", + timestamp = 0L, + ) + + private fun createRingingCallNotificationCreator( + matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), + imageLoaderHolder: FakeImageLoaderHolder = FakeImageLoaderHolder(), + notificationBitmapLoader: FakeNotificationBitmapLoader = FakeNotificationBitmapLoader(), + ) = RingingCallNotificationCreator( + context = InstrumentationRegistry.getInstrumentation().targetContext, + matrixClientProvider = matrixClientProvider, + imageLoaderHolder = imageLoaderHolder, + notificationBitmapLoader = notificationBitmapLoader, + ) +} diff --git a/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt similarity index 96% rename from features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt rename to features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt index 30579ad99c..b734a8ff5c 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt @@ -21,7 +21,11 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.features.call.CallType +import io.element.android.features.call.api.CallType +import io.element.android.features.call.impl.ui.CallScreenEvents +import io.element.android.features.call.impl.ui.CallScreenNavigator +import io.element.android.features.call.impl.ui.CallScreenPresenter +import io.element.android.features.call.utils.FakeActiveCallManager import io.element.android.features.call.utils.FakeCallWidgetProvider import io.element.android.features.call.utils.FakeWidgetMessageInterceptor import io.element.android.libraries.architecture.AsyncData @@ -254,6 +258,7 @@ class CallScreenPresenterTest { widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), + activeCallManager: FakeActiveCallManager = FakeActiveCallManager(), screenTracker: ScreenTracker = FakeScreenTracker(), ): CallScreenPresenter { val userAgentProvider = object : UserAgentProvider { @@ -270,8 +275,9 @@ class CallScreenPresenterTest { clock = clock, dispatchers = dispatchers, matrixClientsProvider = matrixClientsProvider, - screenTracker = screenTracker, appCoroutineScope = this, + activeCallManager = activeCallManager, + screenTracker = screenTracker, ) } } diff --git a/features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt similarity index 92% rename from features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt rename to features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt index 23e0cf9027..c280a1a61d 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt @@ -16,6 +16,8 @@ package io.element.android.features.call.ui +import io.element.android.features.call.impl.ui.CallScreenNavigator + class FakeCallScreenNavigator : CallScreenNavigator { var closeCalled = false private set diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt similarity index 99% rename from features/call/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt rename to features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt index 093b6aab3b..595f5aa41d 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt @@ -17,6 +17,7 @@ package io.element.android.features.call.utils import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.impl.utils.CallIntentDataParser import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt new file mode 100644 index 0000000000..3ae95665be --- /dev/null +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.utils + +import androidx.core.app.NotificationManagerCompat +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator +import io.element.android.features.call.impl.utils.ActiveCall +import io.element.android.features.call.impl.utils.CallState +import io.element.android.features.call.impl.utils.DefaultActiveCallManager +import io.element.android.features.call.test.aCallNotificationData +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID_2 +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.FakeMatrixClientProvider +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.push.api.notifications.ForegroundServiceType +import io.element.android.libraries.push.api.notifications.NotificationIdProvider +import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder +import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler +import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DefaultActiveCallManagerTest { + private val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.INCOMING_CALL) + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `registerIncomingCall - sets the incoming call as active`() = runTest { + val notificationManagerCompat = mockk(relaxed = true) + val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) + + assertThat(manager.activeCall.value).isNull() + + val callNotificationData = aCallNotificationData() + manager.registerIncomingCall(callNotificationData) + + assertThat(manager.activeCall.value).isEqualTo( + ActiveCall( + sessionId = callNotificationData.sessionId, + roomId = callNotificationData.roomId, + callState = CallState.Ringing(callNotificationData) + ) + ) + + runCurrent() + + verify { notificationManagerCompat.notify(notificationId, any()) } + } + + @Test + fun `registerIncomingCall - when there is an already active call does nothing`() = runTest { + val manager = createActiveCallManager() + + // Register existing call + val callNotificationData = aCallNotificationData() + manager.registerIncomingCall(callNotificationData) + val activeCall = manager.activeCall.value + + // Now add a new call + manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2)) + + assertThat(manager.activeCall.value).isEqualTo(activeCall) + assertThat(manager.activeCall.value?.roomId).isNotEqualTo(A_ROOM_ID_2) + } + + @Test + fun `incomingCallTimedOut - when there isn't an active call does nothing`() = runTest { + val addMissedCallNotificationLambda = lambdaRecorder { _, _, _ -> } + val manager = createActiveCallManager( + onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda) + ) + + manager.incomingCallTimedOut() + + addMissedCallNotificationLambda.assertions().isNeverCalled() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `incomingCallTimedOut - when there is an active call removes it and adds a missed call notification`() = runTest { + val notificationManagerCompat = mockk(relaxed = true) + val addMissedCallNotificationLambda = lambdaRecorder { _, _, _ -> } + val manager = createActiveCallManager( + onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda), + notificationManagerCompat = notificationManagerCompat, + ) + + manager.registerIncomingCall(aCallNotificationData()) + assertThat(manager.activeCall.value).isNotNull() + + manager.incomingCallTimedOut() + assertThat(manager.activeCall.value).isNull() + + runCurrent() + + addMissedCallNotificationLambda.assertions().isCalledOnce() + + verify { notificationManagerCompat.cancel(notificationId) } + } + + @Test + fun `hungUpCall - removes existing call`() = runTest { + val notificationManagerCompat = mockk(relaxed = true) + val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) + + manager.registerIncomingCall(aCallNotificationData()) + assertThat(manager.activeCall.value).isNotNull() + + manager.hungUpCall() + assertThat(manager.activeCall.value).isNull() + + verify { notificationManagerCompat.cancel(notificationId) } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest { + val notificationManagerCompat = mockk(relaxed = true) + val sendCallNotifyLambda = lambdaRecorder> { Result.success(Unit) } + val room = FakeMatrixRoom(sendCallNotificationIfNeededResult = sendCallNotifyLambda) + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val manager = createActiveCallManager( + matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }), + notificationManagerCompat = notificationManagerCompat, + ) + assertThat(manager.activeCall.value).isNull() + + manager.joinedCall(A_SESSION_ID, A_ROOM_ID) + assertThat(manager.activeCall.value).isEqualTo( + ActiveCall( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + callState = CallState.InCall, + ) + ) + + runCurrent() + + sendCallNotifyLambda.assertions().isCalledOnce() + verify { notificationManagerCompat.cancel(notificationId) } + } + + private fun TestScope.createActiveCallManager( + matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), + onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(), + notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true), + ) = DefaultActiveCallManager( + coroutineScope = this, + matrixClientProvider = matrixClientProvider, + onMissedCallNotificationHandler = onMissedCallNotificationHandler, + ringingCallNotificationCreator = RingingCallNotificationCreator( + context = InstrumentationRegistry.getInstrumentation().targetContext, + matrixClientProvider = matrixClientProvider, + imageLoaderHolder = FakeImageLoaderHolder(), + notificationBitmapLoader = FakeNotificationBitmapLoader(), + ), + notificationManagerCompat = notificationManagerCompat, + ) +} diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt similarity index 98% rename from features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt rename to features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt index 0b7e2ce953..a41b5f0296 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt @@ -17,6 +17,7 @@ package io.element.android.features.call.utils import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.impl.utils.DefaultCallWidgetProvider import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt new file mode 100644 index 0000000000..763d7bf8d5 --- /dev/null +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.utils + +import io.element.android.features.call.impl.notifications.CallNotificationData +import io.element.android.features.call.impl.utils.ActiveCall +import io.element.android.features.call.impl.utils.ActiveCallManager +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeActiveCallManager( + var registerIncomingCallResult: (CallNotificationData) -> Unit = {}, + var incomingCallTimedOutResult: () -> Unit = {}, + var hungUpCallResult: () -> Unit = {}, + var joinedCallResult: (SessionId, RoomId) -> Unit = { _, _ -> }, +) : ActiveCallManager { + override val activeCall = MutableStateFlow(null) + + override fun registerIncomingCall(notificationData: CallNotificationData) { + registerIncomingCallResult(notificationData) + } + + override fun incomingCallTimedOut() { + incomingCallTimedOutResult() + } + + override fun hungUpCall() { + hungUpCallResult() + } + + override fun joinedCall(sessionId: SessionId, roomId: RoomId) { + joinedCallResult(sessionId, roomId) + } + + fun setActiveCall(value: ActiveCall?) { + this.activeCall.value = value + } +} diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt similarity index 95% rename from features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt rename to features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt index c9e9ebb2ae..b957122a3f 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt @@ -16,6 +16,7 @@ package io.element.android.features.call.utils +import io.element.android.features.call.impl.utils.CallWidgetProvider import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt similarity index 93% rename from features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt rename to features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt index 5a312250b0..0ef5046028 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt @@ -16,6 +16,7 @@ package io.element.android.features.call.utils +import io.element.android.features.call.impl.utils.WidgetMessageInterceptor import kotlinx.coroutines.flow.MutableSharedFlow class FakeWidgetMessageInterceptor : WidgetMessageInterceptor { diff --git a/features/call/test/build.gradle.kts b/features/call/test/build.gradle.kts new file mode 100644 index 0000000000..729811f434 --- /dev/null +++ b/features/call/test/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-compose-library") + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.call.test" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + + api(projects.features.call.api) + implementation(projects.features.call.impl) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrix.test) +} diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt b/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt new file mode 100644 index 0000000000..c69e4b9e6c --- /dev/null +++ b/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.test + +import io.element.android.features.call.impl.notifications.CallNotificationData +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_NAME + +fun aCallNotificationData( + sessionId: SessionId = A_SESSION_ID, + roomId: RoomId = A_ROOM_ID, + eventId: EventId = AN_EVENT_ID, + senderId: UserId = A_USER_ID_2, + roomName: String = A_ROOM_NAME, + senderName: String? = A_USER_NAME, + avatarUrl: String? = AN_AVATAR_URL, + notificationChannelId: String = "channel_id", + timestamp: Long = 0L, +): CallNotificationData = CallNotificationData( + sessionId = sessionId, + roomId = roomId, + eventId = eventId, + senderId = senderId, + roomName = roomName, + senderName = senderName, + avatarUrl = avatarUrl, + notificationChannelId = notificationChannelId, + timestamp = timestamp, +) diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt new file mode 100644 index 0000000000..cda9cf98f0 --- /dev/null +++ b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.test + +import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.ElementCallEntryPoint +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId + +class FakeElementCallEntryPoint( + var startCallResult: (CallType) -> Unit = {}, + var handleIncomingCallResult: (CallType.RoomCall, EventId, UserId, String?, String?, String?, String) -> Unit = { _, _, _, _, _, _, _ -> } +) : ElementCallEntryPoint { + override fun startCall(callType: CallType) { + startCallResult(callType) + } + + override fun handleIncomingCall( + callType: CallType.RoomCall, + eventId: EventId, + senderId: UserId, + roomName: String?, + senderName: String?, + avatarUrl: String?, + timestamp: Long, + notificationChannelId: String + ) { + handleIncomingCallResult(callType, eventId, senderId, roomName, senderName, avatarUrl, notificationChannelId) + } +} diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index df0c4dff7a..6032d195e6 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -39,7 +39,7 @@ dependencies { anvil(projects.anvilcodegen) api(projects.features.messages.api) implementation(projects.appconfig) - implementation(projects.features.call) + implementation(projects.features.call.api) implementation(projects.features.location.api) implementation(projects.features.poll.api) implementation(projects.libraries.androidutils) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 6665c5fa2a..8859c4ae03 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -16,7 +16,6 @@ package io.element.android.features.messages.impl -import android.content.Context import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -31,8 +30,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.Interaction import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.call.CallType -import io.element.android.features.call.ui.ElementCallActivity +import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.location.api.Location import io.element.android.features.location.api.SendLocationEntryPoint import io.element.android.features.location.api.ShowLocationEntryPoint @@ -58,7 +57,6 @@ import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.overlay.Overlay import io.element.android.libraries.architecture.overlay.operation.show -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.EventId @@ -78,11 +76,11 @@ import kotlinx.parcelize.Parcelize class MessagesFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - @ApplicationContext private val context: Context, private val matrixClient: MatrixClient, private val sendLocationEntryPoint: SendLocationEntryPoint, private val showLocationEntryPoint: ShowLocationEntryPoint, private val createPollEntryPoint: CreatePollEntryPoint, + private val elementCallEntryPoint: ElementCallEntryPoint, private val analyticsService: AnalyticsService, ) : BaseFlowNode( backstack = BackStack( @@ -188,12 +186,12 @@ class MessagesFlowNode @AssistedInject constructor( } override fun onJoinCallClick(roomId: RoomId) { - val inputs = CallType.RoomCall( + val callType = CallType.RoomCall( sessionId = matrixClient.sessionId, roomId = roomId, ) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) - ElementCallActivity.start(context, inputs) + elementCallEntryPoint.startCall(callType) } } val inputs = MessagesNode.Inputs( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt index 4b6d28bd89..58cf11c4a7 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt @@ -173,6 +173,7 @@ class TimelineControllerTest { matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline)) val sut = TimelineController(matrixRoom) sut.activeTimelineFlow().test { + sut.focusOnEvent(AN_EVENT_ID) awaitItem().also { state -> assertThat(state).isEqualTo(liveTimeline) } diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 037165e4ee..b90a0e679d 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -56,7 +56,7 @@ dependencies { api(projects.libraries.usersearch.api) api(projects.services.apperror.api) implementation(libs.coil.compose) - implementation(projects.features.call) + implementation(projects.features.call.api) implementation(projects.features.createroom.api) implementation(projects.features.leaveroom.api) implementation(projects.features.userprofile.shared) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 9b3cb0f8bc..4a34d64ad0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -16,7 +16,6 @@ package io.element.android.features.roomdetails.impl -import android.content.Context import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -30,8 +29,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.Interaction import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.call.CallType -import io.element.android.features.call.ui.ElementCallActivity +import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.poll.api.history.PollHistoryEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode @@ -46,7 +45,6 @@ import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.core.mimetype.MimeTypes -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -62,8 +60,8 @@ import kotlinx.parcelize.Parcelize class RoomDetailsFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - @ApplicationContext private val context: Context, private val pollHistoryEntryPoint: PollHistoryEntryPoint, + private val elementCallEntryPoint: ElementCallEntryPoint, private val room: MatrixRoom, private val analyticsService: AnalyticsService, ) : BaseFlowNode( @@ -147,7 +145,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( roomId = room.roomId, ) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) - ElementCallActivity.start(context, inputs) + elementCallEntryPoint.startCall(inputs) } } createNode(buildContext, listOf(roomDetailsCallback)) @@ -195,7 +193,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( } override fun onStartCall(dmRoomId: RoomId) { - ElementCallActivity.start(context, CallType.RoomCall(sessionId = room.sessionId, roomId = dmRoomId)) + elementCallEntryPoint.startCall(CallType.RoomCall(roomId = dmRoomId, sessionId = room.sessionId)) } } val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt index 202766e4ac..ab19c0b522 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt @@ -70,7 +70,10 @@ private fun Content(desktopApplicationName: String) { add(AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_2))) add( annotatedTextWithBold( - text = stringResource(R.string.screen_create_new_recovery_key_list_item_3), + text = stringResource( + id = R.string.screen_create_new_recovery_key_list_item_3, + stringResource(R.string.screen_create_new_recovery_key_list_item_3_reset_all) + ), boldText = stringResource(R.string.screen_create_new_recovery_key_list_item_3_reset_all) ) ) diff --git a/features/userprofile/impl/build.gradle.kts b/features/userprofile/impl/build.gradle.kts index e41524abb2..3837e81f9b 100644 --- a/features/userprofile/impl/build.gradle.kts +++ b/features/userprofile/impl/build.gradle.kts @@ -46,7 +46,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.androidutils) implementation(projects.libraries.mediaviewer.api) - implementation(projects.features.call) + implementation(projects.features.call.api) api(projects.features.userprofile.api) api(projects.features.userprofile.shared) implementation(libs.coil.compose) diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt index 9101254126..d4335f5d3a 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt @@ -16,7 +16,6 @@ package io.element.android.features.userprofile.impl -import android.content.Context import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -29,8 +28,8 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.call.CallType -import io.element.android.features.call.ui.ElementCallActivity +import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint import io.element.android.features.userprofile.impl.root.UserProfileNode import io.element.android.features.userprofile.shared.UserProfileNodeHelper @@ -40,7 +39,6 @@ import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs import io.element.android.libraries.core.mimetype.MimeTypes -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.media.MediaSource @@ -53,7 +51,7 @@ import kotlinx.parcelize.Parcelize class UserProfileFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - @ApplicationContext private val context: Context, + private val elementCallEntryPoint: ElementCallEntryPoint, private val sessionIdHolder: CurrentSessionIdHolder, ) : BaseFlowNode( backstack = BackStack( @@ -84,7 +82,7 @@ class UserProfileFlowNode @AssistedInject constructor( } override fun onStartCall(dmRoomId: RoomId) { - ElementCallActivity.start(context, CallType.RoomCall(sessionId = sessionIdHolder.current, roomId = dmRoomId)) + elementCallEntryPoint.startCall(CallType.RoomCall(sessionId = sessionIdHolder.current, roomId = dmRoomId)) } } val params = UserProfileNode.UserProfileInputs(userId = inputs().userId) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 2dc6c8875f..54791f8505 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp enum class AvatarSize(val dp: Dp) { CurrentUserTopBar(32.dp), + IncomingCall(140.dp), RoomHeader(96.dp), RoomListItem(52.dp), diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index a4619635c6..7ca6080f11 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -332,5 +332,10 @@ interface MatrixRoom : Closeable { */ suspend fun getPermalinkFor(eventId: EventId): Result + /** + * Send an Element Call started notification if needed. + */ + suspend fun sendCallNotificationIfNeeded(): Result + override fun close() = destroy() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt index 67b57b6a07..8367f70725 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt @@ -72,6 +72,7 @@ object EventType { const val CALL_NEGOTIATE = "m.call.negotiate" const val CALL_REJECT = "m.call.reject" const val CALL_HANGUP = "m.call.hangup" + const val CALL_NOTIFY = "m.call.notify" // This type is not processed by the client, just sent to the server const val CALL_REPLACES = "m.call.replaces" @@ -94,6 +95,7 @@ object EventType { type == CALL_SELECT_ANSWER || type == CALL_NEGOTIATE || type == CALL_REJECT || - type == CALL_REPLACES + type == CALL_REPLACES || + type == CALL_NOTIFY } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 2827ad366c..aa7a90c540 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -596,6 +596,10 @@ class RustMatrixRoom( innerRoom.matrixToEventPermalink(eventId.value) } + override suspend fun sendCallNotificationIfNeeded(): Result = runCatching { + innerRoom.sendCallNotificationIfNeeded() + } + private fun createTimeline( timeline: InnerTimeline, isLive: Boolean, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 4b4a8e1af0..f220144485 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -86,6 +86,7 @@ class FakeMatrixRoom( override val liveTimeline: Timeline = FakeTimeline(), private var roomPermalinkResult: () -> Result = { Result.success("room link") }, private var eventPermalinkResult: (EventId) -> Result = { Result.success("event link") }, + var sendCallNotificationIfNeededResult: () -> Result = { Result.success(Unit) }, canRedactOwn: Boolean = false, canRedactOther: Boolean = false, ) : MatrixRoom { @@ -528,6 +529,10 @@ class FakeMatrixRoom( theme: String?, ): Result = generateWidgetWebViewUrlResult + override suspend fun sendCallNotificationIfNeeded(): Result { + return sendCallNotificationIfNeededResult() + } + override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result = getWidgetDriverResult fun givenRoomMembersState(state: MatrixRoomMembersState) { diff --git a/libraries/push/api/build.gradle.kts b/libraries/push/api/build.gradle.kts index c4d78b432d..d043254933 100644 --- a/libraries/push/api/build.gradle.kts +++ b/libraries/push/api/build.gradle.kts @@ -24,6 +24,7 @@ android { dependencies { implementation(libs.androidx.corektx) implementation(libs.coroutines.core) + implementation(libs.coil.compose) implementation(projects.libraries.matrix.api) implementation(projects.libraries.pushproviders.api) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt new file mode 100644 index 0000000000..6b56eb9e2a --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.api.notifications + +import android.graphics.Bitmap +import androidx.core.graphics.drawable.IconCompat +import coil.ImageLoader + +interface NotificationBitmapLoader { + /** + * Get icon of a room. + * @param path mxc url + * @param imageLoader Coil image loader + */ + suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? + + /** + * Get icon of a user. + * Before Android P, this does nothing because the icon won't be used + * @param path mxc url + * @param imageLoader Coil image loader + */ + suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationIdProvider.kt similarity index 63% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt rename to libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationIdProvider.kt index 050edfcc11..0edfe405ac 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationIdProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2024 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,13 +14,12 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.notifications +package io.element.android.libraries.push.api.notifications import io.element.android.libraries.matrix.api.core.SessionId -import javax.inject.Inject import kotlin.math.abs -class NotificationIdProvider @Inject constructor() { +object NotificationIdProvider { fun getSummaryNotificationId(sessionId: SessionId): Int { return getOffset(sessionId) + SUMMARY_NOTIFICATION_ID } @@ -41,16 +40,30 @@ class NotificationIdProvider @Inject constructor() { return getOffset(sessionId) + FALLBACK_NOTIFICATION_ID } + fun getCallNotificationId(sessionId: SessionId): Int { + return getOffset(sessionId) + ROOM_CALL_NOTIFICATION_ID + } + + fun getForegroundServiceNotificationId(type: ForegroundServiceType): Int { + return type.id * 10 + FOREGROUND_SERVICE_NOTIFICATION_ID + } + private fun getOffset(sessionId: SessionId): Int { // Compute a int from a string with a low risk of collision. return abs(sessionId.value.hashCode() % 100_000) * 10 } - companion object { - private const val FALLBACK_NOTIFICATION_ID = -1 - private const val SUMMARY_NOTIFICATION_ID = 0 - private const val ROOM_MESSAGES_NOTIFICATION_ID = 1 - private const val ROOM_EVENT_NOTIFICATION_ID = 2 - private const val ROOM_INVITATION_NOTIFICATION_ID = 3 - } + private const val FALLBACK_NOTIFICATION_ID = -1 + private const val SUMMARY_NOTIFICATION_ID = 0 + private const val ROOM_MESSAGES_NOTIFICATION_ID = 1 + private const val ROOM_EVENT_NOTIFICATION_ID = 2 + private const val ROOM_INVITATION_NOTIFICATION_ID = 3 + private const val ROOM_CALL_NOTIFICATION_ID = 3 + + private const val FOREGROUND_SERVICE_NOTIFICATION_ID = 4 +} + +enum class ForegroundServiceType(val id: Int) { + INCOMING_CALL(1), + ONGOING_CALL(2), } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/OnMissedCallNotificationHandler.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/OnMissedCallNotificationHandler.kt new file mode 100644 index 0000000000..40bf786536 --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/OnMissedCallNotificationHandler.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.api.notifications + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId + +/** + * Handles missed calls by creating a new notification. + */ +interface OnMissedCallNotificationHandler { + /** + * Adds a missed call notification. + */ + suspend fun addMissedCallNotification( + sessionId: SessionId, + roomId: RoomId, + eventId: EventId, + ) +} diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 85b1d80942..8970f1e3d0 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(projects.libraries.preferences.api) implementation(projects.libraries.uiStrings) implementation(projects.libraries.troubleshoot.api) + implementation(projects.features.call.api) api(projects.libraries.pushproviders.api) api(projects.libraries.pushstore.api) api(projects.libraries.push.api) @@ -76,6 +77,7 @@ dependencies { testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushstore.test) testImplementation(projects.tests.testutils) + testImplementation(projects.features.call.test) testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.toolbox.impl) testImplementation(projects.services.toolbox.test) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt index 0746e9c5cd..01b9837f3b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt @@ -22,6 +22,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.push.api.notifications.NotificationIdProvider import javax.inject.Inject interface ActiveNotificationsProvider { @@ -37,7 +38,6 @@ interface ActiveNotificationsProvider { @ContributesBinding(AppScope::class) class DefaultActiveNotificationsProvider @Inject constructor( private val notificationManager: NotificationManagerCompat, - private val notificationIdProvider: NotificationIdProvider, ) : ActiveNotificationsProvider { override fun getAllNotifications(): List { return notificationManager.activeNotifications @@ -48,22 +48,22 @@ class DefaultActiveNotificationsProvider @Inject constructor( } override fun getMembershipNotificationForSession(sessionId: SessionId): List { - val notificationId = notificationIdProvider.getRoomInvitationNotificationId(sessionId) + val notificationId = NotificationIdProvider.getRoomInvitationNotificationId(sessionId) return getNotificationsForSession(sessionId).filter { it.id == notificationId } } override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List { - val notificationId = notificationIdProvider.getRoomMessagesNotificationId(sessionId) + val notificationId = NotificationIdProvider.getRoomMessagesNotificationId(sessionId) return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value } } override fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List { - val notificationId = notificationIdProvider.getRoomInvitationNotificationId(sessionId) + val notificationId = NotificationIdProvider.getRoomInvitationNotificationId(sessionId) return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value } } override fun getSummaryNotification(sessionId: SessionId): StatusBarNotification? { - val summaryId = notificationIdProvider.getSummaryNotificationId(sessionId) + val summaryId = NotificationIdProvider.getSummaryNotificationId(sessionId) return getNotificationsForSession(sessionId).find { it.id == summaryId } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt index 78414fddd1..646bbaa621 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt @@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType @@ -51,6 +52,7 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock @@ -150,8 +152,7 @@ class DefaultNotifiableEventResolver @Inject constructor( } NotificationContent.MessageLike.CallAnswer, NotificationContent.MessageLike.CallCandidates, - NotificationContent.MessageLike.CallHangup, - is NotificationContent.MessageLike.CallNotify -> { // TODO CallNotify will be handled separately in the future + NotificationContent.MessageLike.CallHangup -> { Timber.tag(loggerTag.value).d("Ignoring notification for call ${content.javaClass.simpleName}") null } @@ -172,6 +173,44 @@ class DefaultNotifiableEventResolver @Inject constructor( senderAvatarPath = senderAvatarUrl, ) } + is NotificationContent.MessageLike.CallNotify -> { + if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp)) { + NotifiableRingingCallEvent( + sessionId = userId, + roomId = roomId, + eventId = eventId, + roomName = roomDisplayName, + editedEventId = null, + canBeReplaced = true, + timestamp = this.timestamp, + isRedacted = false, + isUpdated = false, + description = stringProvider.getString(R.string.notification_incoming_call), + senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId), + roomAvatarUrl = roomAvatarUrl, + callNotifyType = content.type, + senderId = content.senderId, + senderAvatarUrl = senderAvatarUrl, + ) + } else { + // Create a simple message notification event + buildNotifiableMessageEvent( + sessionId = userId, + senderId = content.senderId, + roomId = roomId, + eventId = eventId, + noisy = true, + timestamp = this.timestamp, + senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId), + body = "☎️ ${stringProvider.getString(R.string.notification_incoming_call)}", + roomName = roomDisplayName, + roomIsDirect = isDirect, + roomAvatarPath = roomAvatarUrl, + senderAvatarPath = senderAvatarUrl, + type = EventType.CALL_NOTIFY, + ) + } + } NotificationContent.MessageLike.KeyVerificationAccept, NotificationContent.MessageLike.KeyVerificationCancel, NotificationContent.MessageLike.KeyVerificationDone, @@ -334,7 +373,8 @@ private fun buildNotifiableMessageEvent( outGoingMessage: Boolean = false, outGoingMessageFailed: Boolean = false, isRedacted: Boolean = false, - isUpdated: Boolean = false + isUpdated: Boolean = false, + type: String = EventType.MESSAGE, ) = NotifiableMessageEvent( sessionId = sessionId, senderId = senderId, @@ -356,5 +396,6 @@ private fun buildNotifiableMessageEvent( outGoingMessage = outGoingMessage, outGoingMessageFailed = outGoingMessageFailed, isRedacted = isRedacted, - isUpdated = isUpdated + isUpdated = isUpdated, + type = type, ) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt similarity index 86% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt index 97232320c2..94ff9b8139 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt @@ -24,23 +24,27 @@ import androidx.core.graphics.drawable.toBitmap import coil.ImageLoader import coil.request.ImageRequest import coil.transform.CircleCropTransformation +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import timber.log.Timber import javax.inject.Inject -class NotificationBitmapLoader @Inject constructor( +@ContributesBinding(AppScope::class) +class DefaultNotificationBitmapLoader @Inject constructor( @ApplicationContext private val context: Context, private val sdkIntProvider: BuildVersionSdkIntProvider, -) { +) : NotificationBitmapLoader { /** * Get icon of a room. * @param path mxc url * @param imageLoader Coil image loader */ - suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? { + override suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? { if (path == null) { return null } @@ -67,7 +71,7 @@ class NotificationBitmapLoader @Inject constructor( * @param path mxc url * @param imageLoader Coil image loader */ - suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? { + override suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? { if (path == null || sdkIntProvider.get() < Build.VERSION_CODES.P) { return null } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index 03c1cd21a2..f485e29847 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import io.element.android.libraries.push.api.notifications.NotificationDrawerManager +import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom import io.element.android.services.appnavstate.api.AppNavigationStateService @@ -53,7 +54,6 @@ private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag. class DefaultNotificationDrawerManager @Inject constructor( private val notificationManager: NotificationManagerCompat, private val notificationRenderer: NotificationRenderer, - private val notificationIdProvider: NotificationIdProvider, private val appNavigationStateService: AppNavigationStateService, coroutineScope: CoroutineScope, private val matrixClientProvider: MatrixClientProvider, @@ -124,7 +124,7 @@ class DefaultNotificationDrawerManager @Inject constructor( * Clear all known message events for a [sessionId]. */ override fun clearAllMessagesEvents(sessionId: SessionId) { - notificationManager.cancel(null, notificationIdProvider.getRoomMessagesNotificationId(sessionId)) + notificationManager.cancel(null, NotificationIdProvider.getRoomMessagesNotificationId(sessionId)) clearSummaryNotificationIfNeeded(sessionId) } @@ -142,7 +142,7 @@ class DefaultNotificationDrawerManager @Inject constructor( * Can also be called when a notification for this room is dismissed by the user. */ override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) { - notificationManager.cancel(roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId)) + notificationManager.cancel(roomId.value, NotificationIdProvider.getRoomMessagesNotificationId(sessionId)) clearSummaryNotificationIfNeeded(sessionId) } @@ -165,7 +165,7 @@ class DefaultNotificationDrawerManager @Inject constructor( * Clear the notifications for a single event. */ override fun clearEvent(sessionId: SessionId, eventId: EventId) { - val id = notificationIdProvider.getRoomEventNotificationId(sessionId) + val id = NotificationIdProvider.getRoomEventNotificationId(sessionId) notificationManager.cancel(eventId.value, id) clearSummaryNotificationIfNeeded(sessionId) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt new file mode 100644 index 0000000000..7a62f53d39 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultOnMissedCallNotificationHandler @Inject constructor( + private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager, + private val notifiableEventResolver: NotifiableEventResolver, +) : OnMissedCallNotificationHandler { + override suspend fun addMissedCallNotification( + sessionId: SessionId, + roomId: RoomId, + eventId: EventId, + ) { + // Resolve the event and add a notification for it, at this point it should no longer be a ringing one + val notifiableEvent = notifiableEventResolver.resolveEvent(sessionId, roomId, eventId) + notifiableEvent?.let { defaultNotificationDrawerManager.onNotifiableEventReceived(it) } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt index aff2cc7fca..a0b7e30218 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt @@ -49,6 +49,8 @@ interface NotificationDataFactory { @JvmName("toNotificationSimpleEvents") @Suppress("INAPPLICABLE_JVM_NAME") fun toNotifications(simpleEvents: List): List + @JvmName("toNotificationFallbackEvents") + @Suppress("INAPPLICABLE_JVM_NAME") fun toNotifications(fallback: List): List fun createSummaryNotification( @@ -130,6 +132,8 @@ class DefaultNotificationDataFactory @Inject constructor( } } + @JvmName("toNotificationFallbackEvents") + @Suppress("INAPPLICABLE_JVM_NAME") override fun toNotifications(fallback: List): List { return fallback.map { event -> OneShotNotification( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt index a1509aaa96..e36966cc9a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -19,10 +19,12 @@ package io.element.android.libraries.push.impl.notifications import coil.ImageLoader import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import timber.log.Timber import javax.inject.Inject @@ -30,7 +32,6 @@ import javax.inject.Inject private val loggerTag = LoggerTag("NotificationRenderer", LoggerTag.NotificationLoggerTag) class NotificationRenderer @Inject constructor( - private val notificationIdProvider: NotificationIdProvider, private val notificationDisplayer: NotificationDisplayer, private val notificationDataFactory: NotificationDataFactory, ) { @@ -59,14 +60,14 @@ class NotificationRenderer @Inject constructor( Timber.tag(loggerTag.value).d("Removing summary notification") notificationDisplayer.cancelNotificationMessage( tag = null, - id = notificationIdProvider.getSummaryNotificationId(currentUser.userId) + id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId) ) } roomNotifications.forEach { notificationData -> notificationDisplayer.showNotificationMessage( tag = notificationData.roomId.value, - id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId), + id = NotificationIdProvider.getRoomMessagesNotificationId(currentUser.userId), notification = notificationData.notification ) } @@ -76,7 +77,7 @@ class NotificationRenderer @Inject constructor( Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.key}") notificationDisplayer.showNotificationMessage( tag = notificationData.key, - id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId), + id = NotificationIdProvider.getRoomInvitationNotificationId(currentUser.userId), notification = notificationData.notification ) } @@ -87,7 +88,7 @@ class NotificationRenderer @Inject constructor( Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.key}") notificationDisplayer.showNotificationMessage( tag = notificationData.key, - id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId), + id = NotificationIdProvider.getRoomEventNotificationId(currentUser.userId), notification = notificationData.notification ) } @@ -98,7 +99,7 @@ class NotificationRenderer @Inject constructor( Timber.tag(loggerTag.value).d("Showing fallback notification") notificationDisplayer.showNotificationMessage( tag = "FALLBACK", - id = notificationIdProvider.getFallbackNotificationId(currentUser.userId), + id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId), notification = fallbackNotifications.first().notification ) } @@ -108,7 +109,7 @@ class NotificationRenderer @Inject constructor( Timber.tag(loggerTag.value).d("Updating summary notification") notificationDisplayer.showNotificationMessage( tag = null, - id = notificationIdProvider.getSummaryNotificationId(currentUser.userId), + id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId), notification = summaryNotification.notification ) } @@ -127,6 +128,8 @@ private fun List.groupByType(): GroupedNotificationEvents { is NotifiableMessageEvent -> roomEvents.add(event.castedToEventType()) is SimpleNotifiableEvent -> simpleEvents.add(event.castedToEventType()) is FallbackNotifiableEvent -> fallbackEvents.add(event.castedToEventType()) + // Nothing should be done for ringing call events as they're not handled here + is NotifiableRingingCallEvent -> {} } } return GroupedNotificationEvents(roomEvents, simpleEvents, invitationEvents, fallbackEvents) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index 3285a7ae90..9fcbf8c152 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -23,6 +23,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.libraries.push.impl.notifications.factories.isSmartReplyError diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt index 8c74f845b7..a40871be4c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt @@ -19,10 +19,15 @@ package io.element.android.libraries.push.impl.notifications.channels import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context +import android.media.AudioAttributes +import android.media.AudioManager +import android.media.RingtoneManager import android.os.Build import androidx.annotation.ChecksSdkIntAtLeast +import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat +import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn @@ -30,15 +35,51 @@ import io.element.android.libraries.push.impl.R import io.element.android.services.toolbox.api.strings.StringProvider import javax.inject.Inject +/* ========================================================================================== + * IDs for channels + * ========================================================================================== */ +private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID" +internal const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" +internal const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID" + +// Legacy channel +private const val CALL_NOTIFICATION_CHANNEL_ID_V2 = "CALL_NOTIFICATION_CHANNEL_ID_V2" + +internal const val CALL_NOTIFICATION_CHANNEL_ID_V3 = "CALL_NOTIFICATION_CHANNEL_ID_V3" +internal const val RINGING_CALL_NOTIFICATION_CHANNEL_ID = "RINGING_CALL_NOTIFICATION_CHANNEL_ID" + /** * on devices >= android O, we need to define a channel for each notifications. */ +interface NotificationChannels { + /** + * Get the channel for incoming call. + * @param ring true if the device should ring when receiving the call. + */ + fun getChannelForIncomingCall(ring: Boolean): String + + /** + * Get the channel for messages. + * @param noisy true if the notification should have sound and vibration. + */ + fun getChannelIdForMessage(noisy: Boolean): String + + /** + * Get the channel for test notifications. + */ + fun getChannelIdForTest(): String +} + +@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) +private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + @SingleIn(AppScope::class) -class NotificationChannels @Inject constructor( +@ContributesBinding(AppScope::class) +class DefaultNotificationChannels @Inject constructor( @ApplicationContext private val context: Context, private val notificationManager: NotificationManagerCompat, private val stringProvider: StringProvider, -) { +) : NotificationChannels { init { createNotificationChannels() } @@ -75,6 +116,9 @@ class NotificationChannels @Inject constructor( } } + // Migration - Create new call channel + notificationManager.deleteNotificationChannel(CALL_NOTIFICATION_CHANNEL_ID_V2) + /** * Default notification importance: shows everywhere, makes noise, but does not visually * intrude. @@ -123,46 +167,52 @@ class NotificationChannels @Inject constructor( } ) + // Register a channel for incoming and in progress call notifications with no ringing notificationManager.createNotificationChannel( NotificationChannel( - CALL_NOTIFICATION_CHANNEL_ID, + CALL_NOTIFICATION_CHANNEL_ID_V3, stringProvider.getString(R.string.notification_channel_call).ifEmpty { "Call" }, NotificationManager.IMPORTANCE_HIGH ) .apply { description = stringProvider.getString(R.string.notification_channel_call) - setSound(null, null) + enableVibration(true) enableLights(true) lightColor = accentColor } ) + + // Register a channel for incoming call notifications which will ring the device when received + val ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE) + notificationManager.createNotificationChannel( + NotificationChannelCompat.Builder( + RINGING_CALL_NOTIFICATION_CHANNEL_ID, + NotificationManagerCompat.IMPORTANCE_MAX, + ) + .setName(stringProvider.getString(R.string.notification_channel_ringing_calls).ifEmpty { "Ringing calls" }) + .setVibrationEnabled(true) + .setSound( + ringtoneUri, + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setLegacyStreamType(AudioManager.STREAM_RING) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .build() + ) + .setDescription(stringProvider.getString(R.string.notification_channel_ringing_calls)) + .setLightsEnabled(true) + .setLightColor(accentColor) + .build() + ) } - private fun getChannel(channelId: String): NotificationChannel? { - return notificationManager.getNotificationChannel(channelId) + override fun getChannelForIncomingCall(ring: Boolean): String { + return if (ring) RINGING_CALL_NOTIFICATION_CHANNEL_ID else CALL_NOTIFICATION_CHANNEL_ID_V3 } - fun getChannelForIncomingCall(fromBg: Boolean): NotificationChannel? { - val notificationChannel = if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID - return getChannel(notificationChannel) - } - - fun getChannelIdForMessage(noisy: Boolean): String { + override fun getChannelIdForMessage(noisy: Boolean): String { return if (noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID } - fun getChannelIdForTest(): String = NOISY_NOTIFICATION_CHANNEL_ID - - companion object { - /* ========================================================================================== - * IDs for channels - * ========================================================================================== */ - private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID" - private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" - private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID" - private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2" - - @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) - private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O - } + override fun getChannelIdForTest(): String = NOISY_NOTIFICATION_CHANNEL_ID } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt index cd6b32086b..5c8dd1dc77 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt @@ -35,9 +35,10 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId +import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.libraries.push.impl.R -import io.element.android.libraries.push.impl.notifications.NotificationBitmapLoader import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug @@ -129,12 +130,16 @@ class DefaultNotificationCreator @Inject constructor( val smallIcon = CommonDrawables.ic_notification_small - val channelId = notificationChannels.getChannelIdForMessage(roomInfo.shouldBing) + val containsMissedCall = events.any { it.type == EventType.CALL_NOTIFY } + val channelId = if (containsMissedCall) { + notificationChannels.getChannelForIncomingCall(false) + } else { + notificationChannels.getChannelIdForMessage(noisy = roomInfo.shouldBing) + } val builder = if (existingNotification != null) { NotificationCompat.Builder(context, existingNotification) } else { NotificationCompat.Builder(context, channelId) - .setOnlyAlertOnce(roomInfo.isUpdated) // A category allows groups of notifications to be ranked and filtered – per user or system settings. // For example, alarm notifications should display before promo notifications, or message from known contact // that can be displayed in not disturb mode if white listed (the later will need compat28.x) @@ -210,6 +215,11 @@ class DefaultNotificationCreator @Inject constructor( setLargeIcon(largeIcon) } setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId)) + + // If any of the events are of call notify type it means a missed call, set the category to the right value + if (events.any { it.type == EventType.CALL_NOTIFY }) { + setCategory(NotificationCompat.CATEGORY_MISSED_CALL) + } } .setTicker(tickerText) .build() @@ -343,7 +353,6 @@ class DefaultNotificationCreator @Inject constructor( .setWhen(lastMessageTimestamp) .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setSmallIcon(smallIcon) - // set content text to support devices running API level < 24 .setGroup(currentUser.userId.value) // set this notification as the summary for the group .setGroupSummary(true) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt index 015e24d5e6..4f3dbf57b3 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt @@ -51,9 +51,9 @@ data class NotifiableMessageEvent( val outGoingMessage: Boolean = false, val outGoingMessageFailed: Boolean = false, override val isRedacted: Boolean = false, - override val isUpdated: Boolean = false -) : NotifiableEvent { + override val isUpdated: Boolean = false, val type: String = EventType.MESSAGE +) : NotifiableEvent { override val description: String = body ?: "" // Example of value: @@ -69,9 +69,16 @@ fun NotifiableEvent.shouldIgnoreEventInRoom(appNavigationState: AppNavigationSta val currentSessionId = appNavigationState.navigationState.currentSessionId() ?: return false return when (val currentRoomId = appNavigationState.navigationState.currentRoomId()) { null -> false - else -> appNavigationState.isInForeground && - sessionId == currentSessionId && - roomId == currentRoomId && - (this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId() + else -> { + // Never ignore ringing call notifications + if (this is NotifiableRingingCallEvent) { + false + } else { + appNavigationState.isInForeground && + sessionId == currentSessionId && + roomId == currentRoomId && + (this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId() + } + } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt new file mode 100644 index 0000000000..0b9696177d --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications.model + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.CallNotifyType +import java.time.Instant +import kotlin.time.Duration.Companion.seconds + +data class NotifiableRingingCallEvent( + override val sessionId: SessionId, + override val roomId: RoomId, + override val eventId: EventId, + override val editedEventId: EventId?, + override val description: String?, + override val canBeReplaced: Boolean, + override val isRedacted: Boolean, + override val isUpdated: Boolean, + val roomName: String?, + val senderId: UserId, + val senderDisambiguatedDisplayName: String?, + val senderAvatarUrl: String?, + val roomAvatarUrl: String? = null, + val callNotifyType: CallNotifyType, + val timestamp: Long, +) : NotifiableEvent { + companion object { + fun shouldRing(callNotifyType: CallNotifyType, timestamp: Long): Boolean { + val timeout = 10.seconds.inWholeMilliseconds + val elapsed = Instant.now().toEpochMilli() - timestamp + // Only ring if the type is RING and the elapsed time is less than the timeout + return callNotifyType == CallNotifyType.RING && elapsed < timeout + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index 1d3f349364..fda14f294f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -17,11 +17,15 @@ package io.element.android.libraries.push.impl.push import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver +import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels +import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.libraries.push.impl.test.DefaultTestPush import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler import io.element.android.libraries.pushproviders.api.PushData @@ -44,6 +48,8 @@ class DefaultPushHandler @Inject constructor( private val buildMeta: BuildMeta, private val matrixAuthenticationService: MatrixAuthenticationService, private val diagnosticPushHandler: DiagnosticPushHandler, + private val elementCallEntryPoint: ElementCallEntryPoint, + private val notificationChannels: NotificationChannels, ) : PushHandler { /** * Called when message is received. @@ -91,19 +97,33 @@ class DefaultPushHandler @Inject constructor( return } val userPushStore = userPushStoreFactory.getOrCreate(userId) - if (userPushStore.getNotificationEnabledForDevice().first()) { + val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first() + if (areNotificationsEnabled) { val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId) - if (notifiableEvent == null) { - Timber.w("Unable to get a notification data") - return + when (notifiableEvent) { + null -> Timber.tag(loggerTag.value).w("Unable to get a notification data") + is NotifiableRingingCallEvent -> handleRingingCallEvent(notifiableEvent) + else -> onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent) } - onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent) } else { - // TODO We need to check if this is an incoming call Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.") } } catch (e: Exception) { Timber.tag(loggerTag.value).e(e, "## handleInternal() failed") } } + + private fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) { + Timber.i("## handleInternal() : Incoming call.") + elementCallEntryPoint.handleIncomingCall( + callType = CallType.RoomCall(notifiableEvent.sessionId, notifiableEvent.roomId), + eventId = notifiableEvent.eventId, + senderId = notifiableEvent.senderId, + roomName = notifiableEvent.roomName, + senderName = notifiableEvent.senderDisambiguatedDisplayName, + avatarUrl = notifiableEvent.roomAvatarUrl, + timestamp = notifiableEvent.timestamp, + notificationChannelId = notificationChannels.getChannelForIncomingCall(ring = true), + ) + } } diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index 1064d5c31e..e1bfa07d29 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -3,6 +3,7 @@ "Call" "Listening for events" "Noisy notifications" + "Ringing calls" "Silent notifications" "%1$s: %2$d message" @@ -13,6 +14,7 @@ "%d notifications" "Notification" + "Incoming call" "** Failed to send - please open room" "Join" "Reject" diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt index 4f715ed283..70403e1986 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 +import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.mockk.every import io.mockk.mockk import org.junit.Test @@ -33,6 +34,8 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class DefaultActiveNotificationsProviderTest { + private val notificationIdProvider = NotificationIdProvider + @Test fun `getAllNotifications with no active notifications returns empty list`() { val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = emptyList()) @@ -43,7 +46,6 @@ class DefaultActiveNotificationsProviderTest { @Test fun `getAllNotifications with active notifications returns all`() { - val notificationIdProvider = NotificationIdProvider() val activeNotifications = listOf( aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value), aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value), @@ -57,7 +59,6 @@ class DefaultActiveNotificationsProviderTest { @Test fun `getNotificationsForSession returns only notifications for that session id`() { - val notificationIdProvider = NotificationIdProvider() val activeNotifications = listOf( aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value), aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value), @@ -71,7 +72,6 @@ class DefaultActiveNotificationsProviderTest { @Test fun `getMembershipNotificationsForSession returns only membership notifications for that session id`() { - val notificationIdProvider = NotificationIdProvider() val activeNotifications = listOf( aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value,), aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value), @@ -89,7 +89,6 @@ class DefaultActiveNotificationsProviderTest { @Test fun `getMessageNotificationsForRoom returns only message notifications for those session and room ids`() { - val notificationIdProvider = NotificationIdProvider() val activeNotifications = listOf( aStatusBarNotification( id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), @@ -117,7 +116,6 @@ class DefaultActiveNotificationsProviderTest { @Test fun `getMembershipNotificationsForRoom returns only membership notifications for those session and room ids`() { - val notificationIdProvider = NotificationIdProvider() val activeNotifications = listOf( aStatusBarNotification( id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), @@ -145,7 +143,6 @@ class DefaultActiveNotificationsProviderTest { @Test fun `getSummaryNotification returns only the summary notification for that session id if it exists`() { - val notificationIdProvider = NotificationIdProvider() val activeNotifications = listOf( aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value), aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value), @@ -172,7 +169,6 @@ class DefaultActiveNotificationsProviderTest { } return DefaultActiveNotificationsProvider( notificationManager = notificationManager, - notificationIdProvider = NotificationIdProvider(), ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt index 8eebbbbb5c..afcdc90b9d 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt @@ -18,12 +18,15 @@ package io.element.android.libraries.push.impl.notifications import android.content.Context import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.notification.CallNotifyType import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType @@ -48,7 +51,9 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeNotificatio import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.services.toolbox.impl.strings.AndroidStringProvider +import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import kotlinx.coroutines.test.runTest @@ -58,6 +63,7 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config +@Suppress("LargeClass") @RunWith(RobolectricTestRunner::class) class DefaultNotifiableEventResolverTest { @Test @@ -479,6 +485,109 @@ class DefaultNotifiableEventResolverTest { assertThat(result).isEqualTo(expectedResult) } + @Test + fun `resolve CallNotify - ringing`() = runTest { + val timestamp = DefaultSystemClock().epochMillis() + val sut = createDefaultNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.CallNotify( + A_USER_ID_2, + CallNotifyType.RING + ), + timestamp = timestamp, + ) + ) + ) + val expectedResult = NotifiableRingingCallEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + senderId = A_USER_ID_2, + roomName = null, + editedEventId = null, + description = "Incoming call", + timestamp = timestamp, + canBeReplaced = true, + isRedacted = false, + isUpdated = false, + senderDisambiguatedDisplayName = "Bob", + senderAvatarUrl = null, + callNotifyType = CallNotifyType.RING, + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve CallNotify - ring but timed out displays the same as notify`() = runTest { + val sut = createDefaultNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.CallNotify( + A_USER_ID_2, + CallNotifyType.RING + ), + timestamp = 0L, + ) + ) + ) + val expectedResult = NotifiableMessageEvent( + sessionId = A_SESSION_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + noisy = true, + timestamp = 0L, + senderDisambiguatedDisplayName = "Bob", + senderId = UserId("@bob:server.org"), + body = "☎\uFE0F Incoming call", + roomId = A_ROOM_ID, + threadId = null, + roomName = null, + roomIsDirect = false, + canBeReplaced = false, + isRedacted = false, + imageUriString = null, + type = EventType.CALL_NOTIFY, + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve CallNotify - notify`() = runTest { + val sut = createDefaultNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.CallNotify( + A_USER_ID_2, + CallNotifyType.NOTIFY + ) + ) + ) + ) + val expectedResult = NotifiableMessageEvent( + sessionId = A_SESSION_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + noisy = true, + timestamp = A_TIMESTAMP, + senderDisambiguatedDisplayName = "Bob", + senderId = UserId("@bob:server.org"), + body = "☎\uFE0F Incoming call", + roomId = A_ROOM_ID, + threadId = null, + roomName = null, + roomIsDirect = false, + canBeReplaced = false, + isRedacted = false, + imageUriString = null, + type = EventType.CALL_NOTIFY, + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + assertThat(result).isEqualTo(expectedResult) + } + @Test fun `resolve null cases`() { testNull(NotificationContent.MessageLike.CallAnswer) @@ -558,6 +667,7 @@ class DefaultNotifiableEventResolverTest { content: NotificationContent, isDirect: Boolean = false, hasMention: Boolean = false, + timestamp: Long = A_TIMESTAMP, ): NotificationData { return NotificationData( eventId = AN_EVENT_ID, @@ -570,7 +680,7 @@ class DefaultNotifiableEventResolverTest { isDirect = isDirect, isEncrypted = false, isNoisy = false, - timestamp = A_TIMESTAMP, + timestamp = timestamp, content = content, hasMention = hasMention, ) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt index 3924171d60..bbe5e273b2 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt @@ -28,12 +28,13 @@ import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider -import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoaderHolder import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent +import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder import io.element.android.services.appnavstate.api.AppNavigationState import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.NavigationState @@ -164,7 +165,7 @@ class DefaultNotificationDrawerManagerTest { val notificationManager = mockk { every { cancel(any(), any()) } returns Unit } - val summaryId = NotificationIdProvider().getSummaryNotificationId(A_SESSION_ID) + val summaryId = NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID) val activeNotificationsProvider = FakeActiveNotificationsProvider( mutableListOf( mockk { @@ -198,7 +199,6 @@ class DefaultNotificationDrawerManagerTest { return DefaultNotificationDrawerManager( notificationManager = notificationManager, notificationRenderer = NotificationRenderer( - notificationIdProvider = NotificationIdProvider(), notificationDisplayer = DefaultNotificationDisplayer(context, NotificationManagerCompat.from(context)), notificationDataFactory = DefaultNotificationDataFactory( notificationCreator = FakeNotificationCreator(), @@ -208,7 +208,6 @@ class DefaultNotificationDrawerManagerTest { stringProvider = FakeStringProvider(), ), ), - notificationIdProvider = NotificationIdProvider(), appNavigationStateService = appNavigationStateService, coroutineScope = this, matrixClientProvider = matrixClientProvider, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt new file mode 100644 index 0000000000..ce2a698ae1 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.FakeMatrixClientProvider +import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider +import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory +import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer +import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent +import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder +import io.element.android.services.appnavstate.test.FakeAppNavigationStateService +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DefaultOnMissedCallNotificationHandlerTest { + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `addMissedCallNotification - should add missed call notification`() = runTest { + val childScope = CoroutineScope(coroutineContext + SupervisorJob()) + val dataFactory = FakeNotificationDataFactory( + messageEventToNotificationsResult = lambdaRecorder { _, _, _ -> emptyList() } + ) + val defaultOnMissedCallNotificationHandler = DefaultOnMissedCallNotificationHandler( + defaultNotificationDrawerManager = DefaultNotificationDrawerManager( + notificationManager = mockk(relaxed = true), + notificationRenderer = NotificationRenderer( + notificationDisplayer = FakeNotificationDisplayer(), + notificationDataFactory = dataFactory, + ), + appNavigationStateService = FakeAppNavigationStateService(), + coroutineScope = childScope, + matrixClientProvider = FakeMatrixClientProvider(), + imageLoaderHolder = FakeImageLoaderHolder(), + activeNotificationsProvider = FakeActiveNotificationsProvider(), + ), + notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventResult = { _, _, _ -> aNotifiableMessageEvent() }), + ) + + defaultOnMissedCallNotificationHandler.addMissedCallNotification( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + ) + + runCurrent() + + dataFactory.messageEventToNotificationsResult.assertions().isCalledOnce() + + // Cancel the coroutine scope so the test can finish + childScope.cancel() + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt index 8a8b5efd43..2b5b941292 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt @@ -25,8 +25,8 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator -import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent +import io.element.android.libraries.push.test.notifications.FakeImageLoader import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider @@ -212,7 +212,7 @@ fun createRoomGroupMessageCreator( sdkIntProvider: BuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.O), ): RoomGroupMessageCreator { val context = RuntimeEnvironment.getApplication() as Context - val bitmapLoader = NotificationBitmapLoader( + val bitmapLoader = DefaultNotificationBitmapLoader( context = RuntimeEnvironment.getApplication(), sdkIntProvider = sdkIntProvider, ) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt index aff5de7d4b..27f66a7b33 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt @@ -23,13 +23,13 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider -import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent +import io.element.android.libraries.push.test.notifications.FakeImageLoader import io.element.android.services.toolbox.test.strings.FakeStringProvider import kotlinx.coroutines.test.runTest import org.junit.Test @@ -37,6 +37,7 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner private val MY_AVATAR_URL: String? = null + private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID) private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID) private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProviderTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProviderTest.kt index b9664ef577..17f736df86 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProviderTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProviderTest.kt @@ -19,12 +19,13 @@ package io.element.android.libraries.push.impl.notifications import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 +import io.element.android.libraries.push.api.notifications.NotificationIdProvider import org.junit.Test class NotificationIdProviderTest { @Test fun `test notification id provider`() { - val sut = NotificationIdProvider() + val sut = NotificationIdProvider val offsetForASessionId = 305_410 assertThat(sut.getSummaryNotificationId(A_SESSION_ID)).isEqualTo(offsetForASessionId + 0) assertThat(sut.getRoomMessagesNotificationId(A_SESSION_ID)).isEqualTo(offsetForASessionId + 1) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt index 0a5b216ea0..e83c5d2351 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt @@ -20,8 +20,8 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider -import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator @@ -31,6 +31,7 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiable import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.libraries.push.test.notifications.FakeImageLoader import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -61,10 +62,9 @@ class NotificationRendererTest { activeNotificationsProvider = FakeActiveNotificationsProvider(), stringProvider = FakeStringProvider(), ) - private val notificationIdProvider = NotificationIdProvider() + private val notificationIdProvider = NotificationIdProvider private val notificationRenderer = NotificationRenderer( - notificationIdProvider = notificationIdProvider, notificationDisplayer = notificationDisplayer, notificationDataFactory = notificationDataFactory, ) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/FakeNotificationChannels.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/FakeNotificationChannels.kt new file mode 100644 index 0000000000..9b5f8de3be --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/FakeNotificationChannels.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications.channels + +class FakeNotificationChannels( + var channelForIncomingCall: (ring: Boolean) -> String = { _ -> "" }, + var channelIdForMessage: (noisy: Boolean) -> String = { _ -> "" }, + var channelIdForTest: () -> String = { "" } +) : NotificationChannels { + override fun getChannelForIncomingCall(ring: Boolean): String { + return channelForIncomingCall(ring) + } + + override fun getChannelIdForMessage(noisy: Boolean): String { + return channelIdForMessage(noisy) + } + + override fun getChannelIdForTest(): String { + return channelIdForTest() + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt new file mode 100644 index 0000000000..ec87069cd2 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications.channels + +import android.app.NotificationChannel +import android.os.Build +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationManagerCompat +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +class NotificationChannelsTest { + @Test + @Config(sdk = [Build.VERSION_CODES.O]) + fun `init - creates notification channels and migrates old ones`() { + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + } + + createNotificationChannels(notificationManager = notificationManager) + + verify { notificationManager.createNotificationChannel(any()) } + verify { notificationManager.createNotificationChannel(any()) } + verify { notificationManager.deleteNotificationChannel(any()) } + } + + @Test + fun `getChannelForIncomingCall - returns the right channel`() { + val notificationChannels = createNotificationChannels() + + val ringingChannel = notificationChannels.getChannelForIncomingCall(ring = true) + assertThat(ringingChannel).isEqualTo(RINGING_CALL_NOTIFICATION_CHANNEL_ID) + + val normalChannel = notificationChannels.getChannelForIncomingCall(ring = false) + assertThat(normalChannel).isEqualTo(CALL_NOTIFICATION_CHANNEL_ID_V3) + } + + @Test + fun `getChannelIdForMessage - returns the right channel`() { + val notificationChannels = createNotificationChannels() + + assertThat(notificationChannels.getChannelIdForMessage(noisy = true)).isEqualTo(NOISY_NOTIFICATION_CHANNEL_ID) + assertThat(notificationChannels.getChannelIdForMessage(noisy = false)).isEqualTo(SILENT_NOTIFICATION_CHANNEL_ID) + } + + @Test + fun `getChannelIdForTest - returns the right channel`() { + val notificationChannels = createNotificationChannels() + + assertThat(notificationChannels.getChannelIdForTest()).isEqualTo(NOISY_NOTIFICATION_CHANNEL_ID) + } + + private fun createNotificationChannels( + notificationManager: NotificationManagerCompat = mockk(relaxed = true), + ) = DefaultNotificationChannels( + context = InstrumentationRegistry.getInstrumentation().targetContext, + notificationManager = notificationManager, + stringProvider = FakeStringProvider(), + ) +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt index de0c22d1a6..124c71adcf 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt @@ -29,18 +29,20 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader +import io.element.android.libraries.push.impl.notifications.DefaultNotificationBitmapLoader import io.element.android.libraries.push.impl.notifications.NotificationActionIds -import io.element.android.libraries.push.impl.notifications.NotificationBitmapLoader import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo +import io.element.android.libraries.push.impl.notifications.channels.DefaultNotificationChannels import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.RejectInvitationActionFactory -import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent +import io.element.android.libraries.push.test.notifications.FakeImageLoader import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP @@ -284,7 +286,7 @@ fun createNotificationCreator( context: Context = RuntimeEnvironment.getApplication(), buildMeta: BuildMeta = aBuildMeta(), notificationChannels: NotificationChannels = createNotificationChannels(), - bitmapLoader: NotificationBitmapLoader = NotificationBitmapLoader(context, FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.R)), + bitmapLoader: NotificationBitmapLoader = DefaultNotificationBitmapLoader(context, FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.R)), ): NotificationCreator { return DefaultNotificationCreator( context = context, @@ -327,5 +329,5 @@ fun createNotificationCreator( fun createNotificationChannels(): NotificationChannels { val context = RuntimeEnvironment.getApplication() - return NotificationChannels(context, NotificationManagerCompat.from(context), FakeStringProvider("")) + return DefaultNotificationChannels(context, NotificationManagerCompat.from(context), FakeStringProvider("")) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt index 221d3d0878..a35ee45996 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt @@ -46,7 +46,7 @@ class FakeNotificationDataFactory( var inviteToNotificationsResult: LambdaOneParamRecorder, List> = lambdaRecorder { _ -> emptyList() }, var simpleEventToNotificationsResult: LambdaOneParamRecorder, List> = lambdaRecorder { _ -> emptyList() }, var fallbackEventToNotificationsResult: LambdaOneParamRecorder, List> = - lambdaRecorder { _ -> emptyList() }, + lambdaRecorder { _ -> emptyList() }, ) : NotificationDataFactory { override suspend fun toNotifications(messages: List, currentUser: MatrixUser, imageLoader: ImageLoader): List { return messageEventToNotificationsResult(messages, currentUser, imageLoader) @@ -64,6 +64,8 @@ class FakeNotificationDataFactory( return simpleEventToNotificationsResult(simpleEvents) } + @JvmName("toNotificationFallbackEvents") + @Suppress("INAPPLICABLE_JVM_NAME") override fun toNotifications(fallback: List): List { return fallbackEventToNotificationsResult(fallback) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt index c8c041720c..3ce472b02d 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt @@ -18,8 +18,8 @@ package io.element.android.libraries.push.impl.notifications.fake import android.app.Notification import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.impl.notifications.NotificationDisplayer -import io.element.android.libraries.push.impl.notifications.NotificationIdProvider import io.element.android.tests.testutils.lambda.LambdaNoParamRecorder import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder @@ -51,7 +51,7 @@ class FakeNotificationDisplayer( fun verifySummaryCancelled(times: Int = 1) { cancelNotificationMessageResult.assertions().isCalledExactly(times).withSequence( - listOf(value(null), value(NotificationIdProvider().getSummaryNotificationId(A_SESSION_ID))) + listOf(value(null), value(NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID))) ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt index 096ae254b8..cd2c66178f 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt @@ -21,11 +21,16 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.CallNotifyType +import io.element.android.libraries.matrix.api.timeline.item.event.EventType +import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent fun aSimpleNotifiableEvent( @@ -79,6 +84,7 @@ fun aNotifiableMessageEvent( threadId: ThreadId? = null, isRedacted: Boolean = false, timestamp: Long = 0, + type: String = EventType.MESSAGE, ) = NotifiableMessageEvent( sessionId = sessionId, eventId = eventId, @@ -94,5 +100,34 @@ fun aNotifiableMessageEvent( roomIsDirect = false, canBeReplaced = false, isRedacted = isRedacted, - imageUriString = null + imageUriString = null, + type = type, +) + +fun anNotifiableCallEvent( + sessionId: SessionId = A_SESSION_ID, + roomId: RoomId = A_ROOM_ID, + eventId: EventId = AN_EVENT_ID, + senderId: UserId = A_USER_ID_2, + senderName: String? = null, + roomAvatarUrl: String? = AN_AVATAR_URL, + senderAvatarUrl: String? = AN_AVATAR_URL, + callNotifyType: CallNotifyType = CallNotifyType.NOTIFY, + timestamp: Long = 0L, +) = NotifiableRingingCallEvent( + sessionId = sessionId, + eventId = eventId, + roomId = roomId, + roomName = "a room name", + editedEventId = null, + description = "description", + timestamp = timestamp, + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + senderDisambiguatedDisplayName = senderName, + senderId = senderId, + roomAvatarUrl = roomAvatarUrl, + senderAvatarUrl = senderAvatarUrl, + callNotifyType = callNotifyType, ) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt index 7739efbd1d..9205a72530 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -19,11 +19,16 @@ package io.element.android.libraries.push.impl.push import app.cash.turbine.test +import io.element.android.features.call.api.CallType +import io.element.android.features.call.test.FakeElementCallEntryPoint import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.CallNotifyType +import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SECRET @@ -31,7 +36,9 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver +import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.fixtures.anNotifiableCallEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.test.DefaultTestPush import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler @@ -47,6 +54,7 @@ import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test +import java.time.Instant class DefaultPushHandlerTest { @Test @@ -220,6 +228,55 @@ class DefaultPushHandlerTest { .isNeverCalled() } + @Test + fun `when ringing call PushData is received, the incoming call will be handled`() = runTest { + val aPushData = PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 0, + clientSecret = A_SECRET, + ) + val handleIncomingCallLambda = lambdaRecorder { _, _, _, _, _, _, _ -> } + val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda) + val defaultPushHandler = createDefaultPushHandler( + elementCallEntryPoint = elementCallEntryPoint, + notifiableEventResult = { _, _, _ -> anNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()) }, + incrementPushCounterResult = {}, + pushClientSecret = FakePushClientSecret( + getUserIdFromSecretResult = { A_USER_ID } + ), + ) + defaultPushHandler.handle(aPushData) + + handleIncomingCallLambda.assertions().isCalledOnce() + } + + @Test + fun `when notify call PushData is received, the incoming call will be treated as a normal notification`() = runTest { + val aPushData = PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 0, + clientSecret = A_SECRET, + ) + val onNotifiableEventReceived = lambdaRecorder {} + val handleIncomingCallLambda = lambdaRecorder { _, _, _, _, _, _, _ -> } + val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda) + val defaultPushHandler = createDefaultPushHandler( + elementCallEntryPoint = elementCallEntryPoint, + onNotifiableEventReceived = onNotifiableEventReceived, + notifiableEventResult = { _, _, _ -> aNotifiableMessageEvent(type = EventType.CALL_NOTIFY) }, + incrementPushCounterResult = {}, + pushClientSecret = FakePushClientSecret( + getUserIdFromSecretResult = { A_USER_ID } + ), + ) + defaultPushHandler.handle(aPushData) + + handleIncomingCallLambda.assertions().isNeverCalled() + onNotifiableEventReceived.assertions().isCalledOnce() + } + @Test fun `when diagnostic PushData is received, the diagnostic push handler is informed `() = runTest { @@ -249,6 +306,8 @@ class DefaultPushHandlerTest { buildMeta: BuildMeta = aBuildMeta(), matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), diagnosticPushHandler: DiagnosticPushHandler = DiagnosticPushHandler(), + elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(), + notificationChannels: FakeNotificationChannels = FakeNotificationChannels(), ): DefaultPushHandler { return DefaultPushHandler( onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived), @@ -263,6 +322,8 @@ class DefaultPushHandlerTest { buildMeta = buildMeta, matrixAuthenticationService = matrixAuthenticationService, diagnosticPushHandler = diagnosticPushHandler, + elementCallEntryPoint = elementCallEntryPoint, + notificationChannels = notificationChannels, ) } } diff --git a/libraries/push/test/build.gradle.kts b/libraries/push/test/build.gradle.kts index 7826c3072b..be8acf362c 100644 --- a/libraries/push/test/build.gradle.kts +++ b/libraries/push/test/build.gradle.kts @@ -24,7 +24,13 @@ android { dependencies { api(projects.libraries.push.api) + implementation(projects.libraries.push.impl) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) implementation(projects.libraries.pushproviders.api) implementation(projects.tests.testutils) + implementation(libs.androidx.core) + implementation(libs.coil.compose) + implementation(libs.coil.test) + implementation(libs.test.robolectric) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoader.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt similarity index 96% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoader.kt rename to libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt index 5c747b45e2..7fd8945e91 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoader.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.notifications.fake +package io.element.android.libraries.push.test.notifications import android.graphics.Color import android.graphics.drawable.ColorDrawable diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoaderHolder.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoaderHolder.kt similarity index 90% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoaderHolder.kt rename to libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoaderHolder.kt index 0e18036e94..57b71007e4 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoaderHolder.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoaderHolder.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2024 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.notifications.fake +package io.element.android.libraries.push.test.notifications import coil.ImageLoader import io.element.android.libraries.matrix.api.MatrixClient diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeOnMissedCallNotificationHandler.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeOnMissedCallNotificationHandler.kt new file mode 100644 index 0000000000..1803be9a0a --- /dev/null +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeOnMissedCallNotificationHandler.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.test.notifications + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler + +class FakeOnMissedCallNotificationHandler( + var addMissedCallNotificationLambda: (SessionId, RoomId, EventId) -> Unit = { _, _, _ -> } +) : OnMissedCallNotificationHandler { + override suspend fun addMissedCallNotification( + sessionId: SessionId, + roomId: RoomId, + eventId: EventId, + ) { + addMissedCallNotificationLambda(sessionId, roomId, eventId) + } +} diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt new file mode 100644 index 0000000000..e21c409ccb --- /dev/null +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.test.notifications.push + +import android.graphics.Bitmap +import androidx.core.graphics.drawable.IconCompat +import coil.ImageLoader +import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader + +class FakeNotificationBitmapLoader( + var getRoomBitmapResult: (String?, ImageLoader) -> Bitmap? = { _, _ -> null }, + var getUserIconResult: (String?, ImageLoader) -> IconCompat? = { _, _ -> null }, +) : NotificationBitmapLoader { + override suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? { + return getRoomBitmapResult(path, imageLoader) + } + + override suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? { + return getUserIconResult(path, imageLoader) + } +} diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index a106be8b5a..e856789944 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -83,6 +83,7 @@ "Quick reply" "Quote" "React" + "Reject" "Remove" "Reply" "Reply in thread" diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt index b227918461..18285488e0 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt @@ -85,6 +85,13 @@ inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: (T1, T2, T3, T4, T5, T6, T7) -> R +): LambdaSevenParamsRecorder { + return LambdaSevenParamsRecorder(ensureNeverCalled, block) +} + inline fun lambdaAnyRecorder( ensureNeverCalled: Boolean = false, noinline block: (List) -> R diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_CallScreenView_null_CallScreenView-Day-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_CallScreenView_null_CallScreenView-Day-0_1_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_CallScreenView_null_CallScreenView-Night-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_CallScreenView_null_CallScreenView-Night-0_2_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Day-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Day-1_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..85e6cc4f6f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Day-1_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52ba9c146f9ceee768b971bee87c7db41321684c77727669acf38e9c32698f96 +size 65711 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Night-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Night-1_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6920a3fd65 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Night-1_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d4019a436d20847db4e2e2b35f96d143790056b867d40dcdc93fe4af33c7f77 +size 59021 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png index 5e56497e00..9c9bcd90e5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22b40382bcb7f296e2b944b954298692c2946693d185976a6b128ec221d1ac5b -size 59235 +oid sha256:d3d1bbc0c03ac483d0047bc4711eb63741c2071f013ee5962a1184e6112bef0c +size 60386 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png index 0e86234e07..5205d00d66 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e5e3a60c51736f2c8e293fccc01cfa969525be86f6f500ffd22ee24413cb659 -size 57864 +oid sha256:b2162a49d87b23c25251c6a8c322ec62eee2eb34e802ab5ce2ecacb637735554 +size 59189 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_10,NEXUS_5,1.0,en].png index 257534fa92..4462ff8798 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:130c300ec896d60e665cd3b11b3d58dd72b54e01365053a677330a52b86314c7 -size 18532 +oid sha256:8b20a53d7a1bb9e6cdb5dbae068c1934d4716986618596b33899caa975070f41 +size 14802 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_11,NEXUS_5,1.0,en].png index 37040e69bc..4e2e4c0254 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:498e1b23a60c563387bbe734fc4ffd3896824d128e1213b194b344091058ae8a -size 21480 +oid sha256:f0ce8e6b948f953ebfb183939e606f3faa0d73ad35c2707b9821f8229bce37e2 +size 19071 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_12,NEXUS_5,1.0,en].png index 5aa2ad2c6f..44a9f12406 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90fbc995b03553699a0ea3cfca65b3c0d800ad91f7405cd59d0f6110aeaa3783 -size 17591 +oid sha256:a1c284262da7cfc1af8aa583d2762084bf1693c44f83bf1cc1de2d74f57dae0e +size 19408 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_13,NEXUS_5,1.0,en].png index 53d55e5152..257534fa92 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_13,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_13,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0d092bc75f8199660f9e527f2712824535ddbcde91da6cefe4b306f668dc026 -size 16279 +oid sha256:130c300ec896d60e665cd3b11b3d58dd72b54e01365053a677330a52b86314c7 +size 18532 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_14,NEXUS_5,1.0,en].png index eab29dd2b2..37040e69bc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_14,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_14,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:206f721ba79222702409f10e70e671e00d855e192387b2fc73dec93bc721765f -size 20933 +oid sha256:498e1b23a60c563387bbe734fc4ffd3896824d128e1213b194b344091058ae8a +size 21480 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_15,NEXUS_5,1.0,en].png index bf6ccb2dd9..5aa2ad2c6f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_15,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_15,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:999fc081f03ee5e80036278cd80098cabfd0b776362ac871c4324b2c1c48bf97 -size 19232 +oid sha256:90fbc995b03553699a0ea3cfca65b3c0d800ad91f7405cd59d0f6110aeaa3783 +size 17591 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_16,NEXUS_5,1.0,en].png index 258b1f2c09..53d55e5152 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_16,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_16,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be7d7c2ad722d9b3b3ca4f9ec4994df387c32fa28480bb8ef2b0d9b57bfcc588 -size 16994 +oid sha256:f0d092bc75f8199660f9e527f2712824535ddbcde91da6cefe4b306f668dc026 +size 16279 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_17,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_17,NEXUS_5,1.0,en].png index 714308dfa4..eab29dd2b2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_17,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_17,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e48c08ee8ff50b9b03bf6af20dfce17db88865b0d1b6db9cbb210d79b8177168 -size 24574 +oid sha256:206f721ba79222702409f10e70e671e00d855e192387b2fc73dec93bc721765f +size 20933 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_18,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_18,NEXUS_5,1.0,en].png index bde780a065..bf6ccb2dd9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_18,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_18,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dffbd73e81a83bec2d11604411d7153bcda8ec44b1e5edc96ad5efd347441137 -size 14509 +oid sha256:999fc081f03ee5e80036278cd80098cabfd0b776362ac871c4324b2c1c48bf97 +size 19232 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_19,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_19,NEXUS_5,1.0,en].png index 6856025214..258b1f2c09 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_19,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_19,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e925ecead3a108881093e83a15fa2702ece8bdb950336489def58c860960e177 -size 13651 +oid sha256:be7d7c2ad722d9b3b3ca4f9ec4994df387c32fa28480bb8ef2b0d9b57bfcc588 +size 16994 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_20,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_20,NEXUS_5,1.0,en].png index 39197031a7..714308dfa4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_20,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_20,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5fd5a514986ca09fd5d59f2ef6c1061510ea22a40ff74ce3a5048464368bdbbe -size 16583 +oid sha256:e48c08ee8ff50b9b03bf6af20dfce17db88865b0d1b6db9cbb210d79b8177168 +size 24574 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_21,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_21,NEXUS_5,1.0,en].png index d013a9b536..bde780a065 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_21,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_21,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5424c7ac0dccbcbf2768d129ec31d2675a729ee17852f1c773afeebf9540f3a2 -size 17349 +oid sha256:dffbd73e81a83bec2d11604411d7153bcda8ec44b1e5edc96ad5efd347441137 +size 14509 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_22,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_22,NEXUS_5,1.0,en].png index fb83908f7b..6856025214 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_22,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_22,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec91c4129a04a5bbd82add9216657d1ef6f61c62cdda8e7969d62a9d3d11b03a -size 16037 +oid sha256:e925ecead3a108881093e83a15fa2702ece8bdb950336489def58c860960e177 +size 13651 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_23,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_23,NEXUS_5,1.0,en].png index 95efeca161..39197031a7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_23,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_23,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbb735a2e27619514a06ebc6e1d84fb6a37d1447a46797e68302dc76655bf323 -size 20700 +oid sha256:5fd5a514986ca09fd5d59f2ef6c1061510ea22a40ff74ce3a5048464368bdbbe +size 16583 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_24,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_24,NEXUS_5,1.0,en].png index a183374ac6..d013a9b536 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_24,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_24,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56b2d5b052303c44434c9752a3d2fcd2c40453370a17c24c2ebf33cc777a7e7b -size 17761 +oid sha256:5424c7ac0dccbcbf2768d129ec31d2675a729ee17852f1c773afeebf9540f3a2 +size 17349 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_25,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_25,NEXUS_5,1.0,en].png index a9bdcd2885..fb83908f7b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_25,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_25,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b09c2ececc08eb3caa70afd3dbd8fd6b34a8d64eb9d9dc99e5ebb1ea2d8d96ea -size 16456 +oid sha256:ec91c4129a04a5bbd82add9216657d1ef6f61c62cdda8e7969d62a9d3d11b03a +size 16037 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_26,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_26,NEXUS_5,1.0,en].png index 261dd54d80..95efeca161 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_26,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_26,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b84949b7a5256c5f3881b9811705eb2f6f2c7b108fb85aa45adf2d4c8afd2b7f -size 21127 +oid sha256:fbb735a2e27619514a06ebc6e1d84fb6a37d1447a46797e68302dc76655bf323 +size 20700 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_27,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_27,NEXUS_5,1.0,en].png index fa5d08e9a2..a183374ac6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_27,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_27,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06310ae4063a59045b5da45b811b9cb29bd483beb86cce8262494177d3925e0f -size 14583 +oid sha256:56b2d5b052303c44434c9752a3d2fcd2c40453370a17c24c2ebf33cc777a7e7b +size 17761 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_28,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_28,NEXUS_5,1.0,en].png index 90d3edbcb6..a9bdcd2885 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_28,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_28,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db4f833beff2bb588fccc1294e76703ff64d427bbf141cf730538a856992d609 -size 13808 +oid sha256:b09c2ececc08eb3caa70afd3dbd8fd6b34a8d64eb9d9dc99e5ebb1ea2d8d96ea +size 16456 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_29,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_29,NEXUS_5,1.0,en].png index 25f01d0c77..261dd54d80 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_29,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_29,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be37247d044b78541c1da7d4e28a3906e89a369f978a486ae1c1aa70b583658f -size 16321 +oid sha256:b84949b7a5256c5f3881b9811705eb2f6f2c7b108fb85aa45adf2d4c8afd2b7f +size 21127 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_3,NEXUS_5,1.0,en].png index 1ae28cd8b1..8f54e4f0be 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29b4f035a161707d8c551f70478651b1c49136489f282f56c1c10af399e182b9 -size 19853 +oid sha256:134390dcc21a7131a88b8060d15ae4a830dd058b1b89518264dd58f503c8b81d +size 22547 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_30,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_30,NEXUS_5,1.0,en].png index 7caaf879aa..fa5d08e9a2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_30,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_30,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85db59158254fa043e53feeddce69cfe89a308a385dde3dcd9a6ff6b94aef7c2 -size 15105 +oid sha256:06310ae4063a59045b5da45b811b9cb29bd483beb86cce8262494177d3925e0f +size 14583 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_31,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_31,NEXUS_5,1.0,en].png index 60077980a3..90d3edbcb6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_31,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_31,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d22ae88ce675b24306df9d598e78600ccb31e203b30f5cfc9e94432bedc518bd -size 14324 +oid sha256:db4f833beff2bb588fccc1294e76703ff64d427bbf141cf730538a856992d609 +size 13808 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_32,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_32,NEXUS_5,1.0,en].png index 21606b47a9..25f01d0c77 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_32,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_32,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3672bb10f51f674dda4d06eced8f5a6e977bd10902d4de7bf9cc4e915d83bb7 -size 16836 +oid sha256:be37247d044b78541c1da7d4e28a3906e89a369f978a486ae1c1aa70b583658f +size 16321 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_33,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_33,NEXUS_5,1.0,en].png index eec3b821df..7caaf879aa 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_33,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_33,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0d524d6f2a6f0cb2b06e8a359c66819dc7e8eb03ad0c17612d35f002f2189f9 -size 15484 +oid sha256:85db59158254fa043e53feeddce69cfe89a308a385dde3dcd9a6ff6b94aef7c2 +size 15105 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_34,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_34,NEXUS_5,1.0,en].png index cf8459e6ba..60077980a3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_34,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_34,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebe050aae72443fdfb7a8cb11e7b5d89a0614907155c3bbebda656c8d44b30a1 -size 15141 +oid sha256:d22ae88ce675b24306df9d598e78600ccb31e203b30f5cfc9e94432bedc518bd +size 14324 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_35,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_35,NEXUS_5,1.0,en].png index 881a35582b..21606b47a9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_35,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_35,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e56fff6052b5c1b3b61a2aa73cad2f03881d86d465dbc59465d63c4aeac3ef4 -size 16371 +oid sha256:e3672bb10f51f674dda4d06eced8f5a6e977bd10902d4de7bf9cc4e915d83bb7 +size 16836 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_36,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_36,NEXUS_5,1.0,en].png index e96833b111..eec3b821df 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_36,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_36,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77d4c098189f99f7091738791218ae663d168d30a442804b8923243c8727598b -size 16145 +oid sha256:f0d524d6f2a6f0cb2b06e8a359c66819dc7e8eb03ad0c17612d35f002f2189f9 +size 15484 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_37,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_37,NEXUS_5,1.0,en].png index 1478eba397..cf8459e6ba 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_37,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_37,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74da4791ade57bbf0b0393143c3f40e9c8aaec36f438ecc85a872ad0f0e03da3 -size 15388 +oid sha256:ebe050aae72443fdfb7a8cb11e7b5d89a0614907155c3bbebda656c8d44b30a1 +size 15141 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_38,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_38,NEXUS_5,1.0,en].png index ee7f8a89cc..881a35582b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_38,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_38,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96518c010196d80e8696ffcaea032468bb4d1edfbe6b1187286da488600363bf -size 17891 +oid sha256:7e56fff6052b5c1b3b61a2aa73cad2f03881d86d465dbc59465d63c4aeac3ef4 +size 16371 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_39,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_39,NEXUS_5,1.0,en].png index fcaf26295f..e96833b111 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_39,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_39,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee072668c6161aa3e2195c2f963bb502112bf6ade57a35be44b013b30aa3d0a9 -size 19308 +oid sha256:77d4c098189f99f7091738791218ae663d168d30a442804b8923243c8727598b +size 16145 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_4,NEXUS_5,1.0,en].png index 8a3b5ced60..99f1aa1671 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5fd0e375d985f2e77eea87650455c7bc5b4e8f1f5f58467811f686316c5449c9 -size 17581 +oid sha256:4d8ee1ebdce2f3024b4f813afd80732afbaff74168d4d7af826e4c5dc1714d15 +size 19159 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_40,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_40,NEXUS_5,1.0,en].png index b63064c734..1478eba397 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_40,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_40,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06d03aa5e848872228a58b399685bc78dfdbad783cb28bfed609dcdc371a168f -size 18520 +oid sha256:74da4791ade57bbf0b0393143c3f40e9c8aaec36f438ecc85a872ad0f0e03da3 +size 15388 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_41,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_41,NEXUS_5,1.0,en].png index 83586a0c51..ee7f8a89cc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_41,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_41,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9dd0232c7847d5c0bb1047cfc6be5d4d43d2cc2e9347aff70a793103b511ad98 -size 21073 +oid sha256:96518c010196d80e8696ffcaea032468bb4d1edfbe6b1187286da488600363bf +size 17891 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png index 598a31fd16..fcaf26295f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f96f83d866d29d3eb713adf9fb8e216414a1eb924a63d0458c77522ba0de0499 -size 16709 +oid sha256:ee072668c6161aa3e2195c2f963bb502112bf6ade57a35be44b013b30aa3d0a9 +size 19308 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png index 0e609afb69..b63064c734 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93e866bf58f381020d9516662332a6c986bfd61acc9954723b27adb52c152d68 -size 15473 +oid sha256:06d03aa5e848872228a58b399685bc78dfdbad783cb28bfed609dcdc371a168f +size 18520 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png index 3bef3f861b..83586a0c51 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f27e368c8c68032dc4eac63012fad459785c42c4063aca99eb6be9d8abe99508 -size 19751 +oid sha256:9dd0232c7847d5c0bb1047cfc6be5d4d43d2cc2e9347aff70a793103b511ad98 +size 21073 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png index cbc5a4ba10..598a31fd16 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e531de46bbc667647a29dc40e8751bad96a089d6765fcddfc69c47147602563 -size 12918 +oid sha256:f96f83d866d29d3eb713adf9fb8e216414a1eb924a63d0458c77522ba0de0499 +size 16709 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png index 1d1884a7ac..0e609afb69 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc57d1ccd6657417469d0d76de066f3a555a5296a83b1cb7168cd64680f97bcd -size 12584 +oid sha256:93e866bf58f381020d9516662332a6c986bfd61acc9954723b27adb52c152d68 +size 15473 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png index b42d0c123d..3bef3f861b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0960f771d8158f52ad925c04c3d9770f4babec0d9ce017958ccd813b4c0012bd -size 13796 +oid sha256:f27e368c8c68032dc4eac63012fad459785c42c4063aca99eb6be9d8abe99508 +size 19751 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_48,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_48,NEXUS_5,1.0,en].png index 488c8a32d2..cbc5a4ba10 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_48,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_48,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe2a0b40572b28fb5934d999ce4fecdf76f7efb61caf390669054c084af594ad -size 18778 +oid sha256:1e531de46bbc667647a29dc40e8751bad96a089d6765fcddfc69c47147602563 +size 12918 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_49,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_49,NEXUS_5,1.0,en].png index a59d5f174c..1d1884a7ac 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_49,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_49,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57358f0990a41b7d22a42b5f06662a8549de79a50126491fa9518bc3ce33bf9a -size 17099 +oid sha256:bc57d1ccd6657417469d0d76de066f3a555a5296a83b1cb7168cd64680f97bcd +size 12584 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_5,NEXUS_5,1.0,en].png index 18efb9a84b..8100c9c10c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42ac664be2f0ed05b1fc844947556b256c823c5bc1bce380cceb1fb50ecd751f -size 25028 +oid sha256:038e9a9b4ef1536e0f77d8330935a8ad6bf2c1664d8d0362fcfca29eb2b3200d +size 30081 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_50,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_50,NEXUS_5,1.0,en].png index e941cef17e..b42d0c123d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_50,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_50,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a5aab04563cc94c4a7636402def1be7d738dd182f733de390d1ad028e99e332 -size 22831 +oid sha256:0960f771d8158f52ad925c04c3d9770f4babec0d9ce017958ccd813b4c0012bd +size 13796 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_51,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_51,NEXUS_5,1.0,en].png index 69af73d4be..488c8a32d2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_51,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_51,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16b2c55f72419639690b09ef46c85c1d284d1da79439adacb38775ae6352c3fb -size 21125 +oid sha256:fe2a0b40572b28fb5934d999ce4fecdf76f7efb61caf390669054c084af594ad +size 18778 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_52,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_52,NEXUS_5,1.0,en].png index 696beb67c6..a59d5f174c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_52,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_52,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a86f83a07283d0c98c64d3a041a6d0b285991f64a34ad372f07d2e04e4d0eff6 -size 19455 +oid sha256:57358f0990a41b7d22a42b5f06662a8549de79a50126491fa9518bc3ce33bf9a +size 17099 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_53,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_53,NEXUS_5,1.0,en].png index ad3bbe7088..e941cef17e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_53,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_53,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6202d80bb0a938bdfca6afef92fc71a00863d2e2c119f62fc61ede3df47cf7b -size 24925 +oid sha256:5a5aab04563cc94c4a7636402def1be7d738dd182f733de390d1ad028e99e332 +size 22831 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_54,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_54,NEXUS_5,1.0,en].png index c670583b4b..69af73d4be 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_54,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_54,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7eb42896831203a1cd8f1ac85b96d33d1048d626f5ee4778733a28d48aeebea4 -size 16744 +oid sha256:16b2c55f72419639690b09ef46c85c1d284d1da79439adacb38775ae6352c3fb +size 21125 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_55,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_55,NEXUS_5,1.0,en].png index 6f8e06f572..696beb67c6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_55,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_55,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93f23803bd0b5633615fb7c1cb40c76a7a1a755f43fb755a08cb390dfd956d75 -size 15967 +oid sha256:a86f83a07283d0c98c64d3a041a6d0b285991f64a34ad372f07d2e04e4d0eff6 +size 19455 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_56,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_56,NEXUS_5,1.0,en].png index dd81493a76..ad3bbe7088 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_56,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_56,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67a8223f107fa05f1c5d66efc85f68325b4bf835e371ad44553bd6d6edd4a201 -size 18491 +oid sha256:c6202d80bb0a938bdfca6afef92fc71a00863d2e2c119f62fc61ede3df47cf7b +size 24925 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_57,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_57,NEXUS_5,1.0,en].png index 3e1cb98594..c670583b4b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_57,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_57,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f600185bb7a9fe6f4e425d8bf43b2743e6169f3a13c34117059aaa863c1b1de0 -size 21593 +oid sha256:7eb42896831203a1cd8f1ac85b96d33d1048d626f5ee4778733a28d48aeebea4 +size 16744 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_58,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_58,NEXUS_5,1.0,en].png index 5a6d361d40..6f8e06f572 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_58,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_58,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2559b3b0e5242e9867aa5c821551a17dbab723e1018476289cb91ed9b9f9ffc4 -size 20736 +oid sha256:93f23803bd0b5633615fb7c1cb40c76a7a1a755f43fb755a08cb390dfd956d75 +size 15967 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_59,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_59,NEXUS_5,1.0,en].png index c1712835ff..dd81493a76 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_59,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_59,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be8c830118af26d3d39a80d5dfa3a750e6e3830a30b066ca91b7ed305a2ee482 -size 23624 +oid sha256:67a8223f107fa05f1c5d66efc85f68325b4bf835e371ad44553bd6d6edd4a201 +size 18491 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_6,NEXUS_5,1.0,en].png index 2740280540..1ae28cd8b1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69dcd8a997542d56ad3c327f3937f9fdc31eecebe30a07a2e6ee26e631c140e6 -size 16047 +oid sha256:29b4f035a161707d8c551f70478651b1c49136489f282f56c1c10af399e182b9 +size 19853 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png index 821589afc5..3e1cb98594 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e70ce111262f22f14ada0993a32d23e1532c6d7a24f71c38d6aba4af6610054 -size 17350 +oid sha256:f600185bb7a9fe6f4e425d8bf43b2743e6169f3a13c34117059aaa863c1b1de0 +size 21593 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png index d0b966d6ec..5a6d361d40 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88fc583b69d153e9027a2df3f98b45a607dcf32838af033b82b3b94e3e03dcc2 -size 16486 +oid sha256:2559b3b0e5242e9867aa5c821551a17dbab723e1018476289cb91ed9b9f9ffc4 +size 20736 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png index 0b8c15e54c..c1712835ff 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:672f829a7227921b7179e91c1ee3ef58ff2b7dab7f131b687c4fe0b9862d2a2c -size 19436 +oid sha256:be8c830118af26d3d39a80d5dfa3a750e6e3830a30b066ca91b7ed305a2ee482 +size 23624 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_63,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_63,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..821589afc5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_63,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e70ce111262f22f14ada0993a32d23e1532c6d7a24f71c38d6aba4af6610054 +size 17350 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_64,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_64,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d0b966d6ec --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_64,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88fc583b69d153e9027a2df3f98b45a607dcf32838af033b82b3b94e3e03dcc2 +size 16486 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_65,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_65,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0b8c15e54c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_65,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:672f829a7227921b7179e91c1ee3ef58ff2b7dab7f131b687c4fe0b9862d2a2c +size 19436 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_7,NEXUS_5,1.0,en].png index 4462ff8798..8a3b5ced60 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b20a53d7a1bb9e6cdb5dbae068c1934d4716986618596b33899caa975070f41 -size 14802 +oid sha256:5fd0e375d985f2e77eea87650455c7bc5b4e8f1f5f58467811f686316c5449c9 +size 17581 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_8,NEXUS_5,1.0,en].png index 4e2e4c0254..18efb9a84b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0ce8e6b948f953ebfb183939e606f3faa0d73ad35c2707b9821f8229bce37e2 -size 19071 +oid sha256:42ac664be2f0ed05b1fc844947556b256c823c5bc1bce380cceb1fb50ecd751f +size 25028 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_9,NEXUS_5,1.0,en].png index 44a9f12406..2740280540 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1c284262da7cfc1af8aa583d2762084bf1693c44f83bf1cc1de2d74f57dae0e -size 19408 +oid sha256:69dcd8a997542d56ad3c327f3937f9fdc31eecebe30a07a2e6ee26e631c140e6 +size 16047 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index d23a72f7a0..1e9281e8c3 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -236,9 +236,10 @@ ] }, { - "name" : ":features:call", + "name" : ":features:call:impl", "includeRegex" : [ - "call_.*" + "call_.*", + "screen_incoming_call.*" ] }, {