From 7c32b35857a80af6b67ddc2635520ee91f237843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 21 Oct 2025 14:10:48 +0200 Subject: [PATCH 1/4] Setting version for the release 25.10.1 --- plugins/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 52c09434d4..673f3cb724 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -44,7 +44,7 @@ private const val versionMonth = 10 * Release number in the month. Value must be in [0,99]. * Do not update this value. it is updated by the release script. */ -private const val versionReleaseNumber = 0 +private const val versionReleaseNumber = 1 object Versions { /** From d2dc9adfe4aba46937283cd4e8be45b4beb8db4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 21 Oct 2025 14:15:10 +0200 Subject: [PATCH 2/4] Adding fastlane file for version 25.10.1 --- fastlane/metadata/android/en-US/changelogs/202510010.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/202510010.txt diff --git a/fastlane/metadata/android/en-US/changelogs/202510010.txt b/fastlane/metadata/android/en-US/changelogs/202510010.txt new file mode 100644 index 0000000000..3c916fed88 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202510010.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes around notifications and UX improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases From 444ae96030cfed4bf2e62b3bd8c0062d1fec5627 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 27 Oct 2025 17:25:56 +0100 Subject: [PATCH 3/4] =?UTF-8?q?Revert=20"Make=20sure=20declining=20a=20cal?= =?UTF-8?q?l=20stops=20observing=20the=20ringing=20call=20state=20(#5?= =?UTF-8?q?=E2=80=A6"=20(#5615)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 10bf5f1c8cfeb4b25ca68b19286718ea588af978. --- .../call/impl/utils/ActiveCallManager.kt | 136 +++++++++--------- .../utils/DefaultActiveCallManagerTest.kt | 45 ------ 2 files changed, 64 insertions(+), 117 deletions(-) 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 index 0f4a96250b..34f46d1ea0 100644 --- 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 @@ -28,9 +28,7 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.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.SessionId -import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import io.element.android.libraries.push.api.notifications.ForegroundServiceType import io.element.android.libraries.push.api.notifications.NotificationIdProvider @@ -41,17 +39,16 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex @@ -183,7 +180,13 @@ class DefaultActiveCallManager( val previousActiveCall = activeCall.value ?: return val notificationData = (previousActiveCall.callState as? CallState.Ringing)?.notificationData ?: return - removeCurrentCall() + activeCall.value = null + if (activeWakeLock?.isHeld == true) { + Timber.tag(tag).d("Releasing partial wakelock after timeout") + activeWakeLock.release() + } + + cancelIncomingCallNotification() if (displayMissedCallNotification) { displayMissedCallNotification(notificationData) @@ -208,16 +211,24 @@ class DefaultActiveCallManager( ?.declineCall(notificationData.eventId) } - removeCurrentCall() + cancelIncomingCallNotification() + if (activeWakeLock?.isHeld == true) { + Timber.tag(tag).d("Releasing partial wakelock after hang up") + activeWakeLock.release() + } + timedOutCallJob?.cancel() + activeCall.value = null } - /** - * Removes the current active call and any associated UI, cancelling the timeouts and wakelocks. - */ override suspend fun joinedCall(callType: CallType) = mutex.withLock { Timber.tag(tag).d("Joined call: $callType") - removeCurrentCall() + cancelIncomingCallNotification() + if (activeWakeLock?.isHeld == true) { + Timber.tag(tag).d("Releasing partial wakelock after joining call") + activeWakeLock.release() + } + timedOutCallJob?.cancel() activeCall.value = ActiveCall( callType = callType, @@ -225,23 +236,6 @@ class DefaultActiveCallManager( ) } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun removeCurrentCall() { - // Cancel and remove the timeout call job, if any - timedOutCallJob?.cancel() - timedOutCallJob = null - - // Remove the active call and cancel the notification - activeCall.value = null - cancelIncomingCallNotification() - - // Also remove any wake locks that may be held - if (activeWakeLock?.isHeld == true) { - Timber.tag(tag).d("Releasing partial wakelock after call declined from another session") - activeWakeLock.release() - } - } - @SuppressLint("MissingPermission") private suspend fun showIncomingCallNotification(notificationData: CallNotificationData) { Timber.tag(tag).d("Displaying ringing call notification") @@ -287,75 +281,73 @@ class DefaultActiveCallManager( @OptIn(ExperimentalCoroutinesApi::class) private fun observeRingingCall() { - val roomForActiveCallFlow: Flow?> = activeCall.mapLatest { activeCall -> - val callType = activeCall?.callType as? CallType.RoomCall ?: return@mapLatest null - val ringingInfo = activeCall.callState as? CallState.Ringing ?: return@mapLatest null - val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run { - Timber.tag(tag).d("Couldn't find session for incoming call: $activeCall") - return@mapLatest null - } - val room = client.getRoom(callType.roomId) ?: run { - Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall") - return@mapLatest null - } + activeCall + .filterNotNull() + .filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall } + .flatMapLatest { activeCall -> + val callType = activeCall.callType as CallType.RoomCall + val ringingInfo = activeCall.callState as CallState.Ringing + val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run { + Timber.tag(tag).d("Couldn't find session for incoming call: $activeCall") + return@flatMapLatest flowOf() + } + val room = client.getRoom(callType.roomId) ?: run { + Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall") + return@flatMapLatest flowOf() + } - Timber.tag(tag).d("Found room for ringing call: ${room.roomId}") - - val eventId = ringingInfo.notificationData.eventId - room to eventId - } - - roomForActiveCallFlow - .flatMapLatest { pair -> - val (room, eventId) = pair - // This will cancel the previous iteration of flatMapLatest if the active call is now null - ?: return@flatMapLatest flowOf() + Timber.tag(tag).d("Found room for ringing call: ${room.roomId}") // If we have declined from another phone we want to stop ringing. - room.subscribeToCallDecline(eventId) + room.subscribeToCallDecline(ringingInfo.notificationData.eventId) .filter { decliner -> Timber.tag(tag).d("Call: $activeCall was declined by $decliner") // only want to listen if the call was declined from another of my sessions, // (we are ringing for an incoming call in a DM) - decliner == room.sessionId + decliner == client.sessionId } } .onEach { decliner -> Timber.tag(tag).d("Call: $activeCall was declined by user from another session") - removeCurrentCall() + // Remove the active call and cancel the notification + activeCall.value = null + if (activeWakeLock?.isHeld == true) { + Timber.tag(tag).d("Releasing partial wakelock after call declined from another session") + activeWakeLock.release() + } + cancelIncomingCallNotification() } .launchIn(coroutineScope) - // This will observe ringing calls and ensure they're terminated if the room call is cancelled or if the user // has joined the call from another session. - roomForActiveCallFlow - .flatMapLatest { pair -> - val (room, _) = pair - // This will cancel the previous iteration of flatMapLatest if the active call is now null - ?: return@flatMapLatest flowOf() - - // We now observe the room info for changes to the active call state and the call participants + activeCall + .filterNotNull() + .filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall } + .flatMapLatest { activeCall -> + val callType = activeCall.callType as CallType.RoomCall + // Get a flow of updated `hasRoomCall` and `activeRoomCallParticipants` values for the room + val room = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull()?.getRoom(callType.roomId) ?: run { + Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall") + return@flatMapLatest flowOf() + } room.roomInfoFlow.map { - val participants = it.activeRoomCallParticipants - Timber.tag(tag).d("Room call status changed for ringing call | hasRoomCall: ${it.hasRoomCall} | participants: $participants") - val userIsInTheCall = room.sessionId in participants - it.hasRoomCall to userIsInTheCall + Timber.tag(tag).d("Has room call status changed for ringing call: ${it.hasRoomCall}") + it.hasRoomCall to (callType.sessionId in it.activeRoomCallParticipants) } } - // Filter out duplicate values + // We only want to check if the room active call status changes .distinctUntilChanged() // Skip the first one, we're not interested in it (if the check below passes, it had to be active anyway) .drop(1) .onEach { (roomHasActiveCall, userIsInTheCall) -> if (!roomHasActiveCall) { - val notificationData = (activeCall.value?.callState as? CallState.Ringing)?.notificationData - removeCurrentCall() - - if (notificationData != null) { - displayMissedCallNotification(notificationData) - } + // The call was cancelled + timedOutCallJob?.cancel() + incomingCallTimedOut(displayMissedCallNotification = true) } else if (userIsInTheCall) { - removeCurrentCall() + // The user joined the call from another session + timedOutCallJob?.cancel() + incomingCallTimedOut(displayMissedCallNotification = false) } } .launchIn(coroutineScope) 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 index d4993cec17..3d1c35df4d 100644 --- 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 @@ -28,7 +28,6 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 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_USER_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.room.FakeBaseRoom @@ -47,7 +46,6 @@ import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.plantTestTimber import io.mockk.coVerify import io.mockk.mockk -import io.mockk.spyk import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -333,49 +331,6 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isNull() } - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun `observeRingingCalls - declining won't do anything if the call was already cancelled`() = runTest { - val room = FakeBaseRoom().apply { - givenRoomInfo(aRoomInfo()) - } - val client = FakeMatrixClient().apply { - givenGetRoomResult(A_ROOM_ID, room) - } - val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }) - val notificationManagerCompat = mockk(relaxed = true) - val manager = spyk( - createActiveCallManager( - matrixClientProvider = matrixClientProvider, - notificationManagerCompat = notificationManagerCompat, - ) - ) - - manager.registerIncomingCall(aCallNotificationData()) - - // Call is active (the other user join the call) - room.givenRoomInfo(aRoomInfo(hasRoomCall = true)) - advanceTimeBy(1) - - // Call is cancelled by us, hanging up - manager.hungUpCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID)) - advanceTimeBy(1) - - verify(exactly = 1) { notificationManagerCompat.cancel(any()) } - verify(exactly = 1) { manager.removeCurrentCall() } - assertThat(manager.activeCall.value).isNull() - assertThat(manager.activeWakeLock?.isHeld).isNull() - - // Simulate that another user declined the call - room.givenDecliner(A_USER_ID_2, AN_EVENT_ID) - advanceTimeBy(1) - - // Check everything stays the same, no extra call to cancelling notifications - verify(exactly = 1) { notificationManagerCompat.cancel(any()) } - verify(exactly = 1) { manager.removeCurrentCall() } - assertThat(manager.activeWakeLock?.isHeld).isNull() - } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun `observeRingingCalls - will do nothing if either the session or the room are not found`() = runTest { From 2fb3f84d2a37e25d0b796dfc9a245c6cab8d9fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 3 Nov 2025 12:29:08 +0100 Subject: [PATCH 4/4] Setting version for the release 25.11.0 --- plugins/src/main/kotlin/Versions.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 673f3cb724..401c76d088 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -38,13 +38,13 @@ private const val versionYear = 25 * Month of the version on 2 digits. Value must be in [1,12]. * Do not update this value. it is updated by the release script. */ -private const val versionMonth = 10 +private const val versionMonth = 11 /** * Release number in the month. Value must be in [0,99]. * Do not update this value. it is updated by the release script. */ -private const val versionReleaseNumber = 1 +private const val versionReleaseNumber = 0 object Versions { /**