From 9b332f7a32f0d81126c94c481b5997da24f46724 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 14 Dec 2020 17:32:54 +0300 Subject: [PATCH 01/57] Test message decryption in a room with 3 members. --- .../timeline/TimelineWithManyMembersTest.kt | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt new file mode 100644 index 0000000000..a0271cb5b9 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020 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 org.matrix.android.sdk.session.room.timeline + +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelineWithManyMembersTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * Ensures when someone sends a message to a crowded room, everyone can decrypt the message. + */ + @Test + fun everyoneShouldDecryptMessage3Members() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobAndSamInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val samSession = cryptoTestData.thirdSession!! + + val aliceRoomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + bobSession.cryptoService().setWarnOnUnknownDevices(false) + samSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + val roomFromSamPOV = samSession.getRoom(aliceRoomId)!! + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) + val samTimeline = roomFromSamPOV.createTimeline(null, TimelineSettings(30)) + bobTimeline.start() + samTimeline.start() + + val firstMessage = "First messages from Alice" + commonTestHelper.sendTextMessage( + roomFromAlicePOV, + firstMessage, + 1) + + bobSession.startSync(true) + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + snapshot.firstOrNull()?.root?.getClearContent()?.toModel()?.body?.startsWith(firstMessage).orFalse() + } + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + } + bobSession.stopSync() + + samSession.startSync(true) + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + snapshot.firstOrNull()?.root?.getClearContent()?.toModel()?.body?.startsWith(firstMessage).orFalse() + } + samTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + } + samSession.stopSync() + } +} From 7e4725c091246c3bb97fb7e45863d31a3b637f1c Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 14 Dec 2020 18:07:20 +0300 Subject: [PATCH 02/57] Update CryptoTestData to handle more than 3 sessions. --- .../android/sdk/common/CryptoTestData.kt | 21 ++++++++++++------- .../android/sdk/common/CryptoTestHelper.kt | 6 +++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt index 76e59d9a90..e01aaef639 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt @@ -18,14 +18,21 @@ package org.matrix.android.sdk.common import org.matrix.android.sdk.api.session.Session -data class CryptoTestData(val firstSession: Session, - val roomId: String, - val secondSession: Session? = null, - val thirdSession: Session? = null) { +data class CryptoTestData(val roomId: String, + val sessions: List = emptyList()) { + + val firstSession: Session + get() = sessions.first() + + val secondSession: Session? + get() = sessions.getOrNull(1) + + val thirdSession: Session? + get() = sessions.getOrNull(2) fun cleanUp(testHelper: CommonTestHelper) { - testHelper.signOutAndClose(firstSession) - secondSession?.let { testHelper.signOutAndClose(it) } - thirdSession?.let { testHelper.signOutAndClose(it) } + sessions.forEach { + testHelper.signOutAndClose(it) + } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index cbb22daf0f..f108cbb105 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -73,7 +73,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { } } - return CryptoTestData(aliceSession, roomId) + return CryptoTestData(roomId, listOf(aliceSession)) } /** @@ -139,7 +139,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { // assertNotNull(roomFromBobPOV.powerLevels) // assertTrue(roomFromBobPOV.powerLevels.maySendMessage(bobSession.myUserId)) - return CryptoTestData(aliceSession, aliceRoomId, bobSession) + return CryptoTestData(aliceRoomId, listOf(aliceSession, bobSession)) } /** @@ -157,7 +157,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { // wait the initial sync SystemClock.sleep(1000) - return CryptoTestData(aliceSession, aliceRoomId, cryptoTestData.secondSession, samSession) + return CryptoTestData(aliceRoomId, listOf(aliceSession, cryptoTestData.secondSession!!, samSession)) } /** From 427dc784fe2d1f131be8e29f930efb70fb74a4e6 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 15 Dec 2020 16:50:49 +0300 Subject: [PATCH 03/57] Support testing a room with many members. --- .../android/sdk/common/CryptoTestData.kt | 2 +- .../android/sdk/common/CryptoTestHelper.kt | 33 ++++++++++++-- .../timeline/TimelineWithManyMembersTest.kt | 43 +++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt index e01aaef639..693ab28da1 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.common import org.matrix.android.sdk.api.session.Session data class CryptoTestData(val roomId: String, - val sessions: List = emptyList()) { + val sessions: MutableList = mutableListOf()) { val firstSession: Session get() = sessions.first() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index f108cbb105..66fc1348d5 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -73,7 +73,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { } } - return CryptoTestData(roomId, listOf(aliceSession)) + return CryptoTestData(roomId, mutableListOf(aliceSession)) } /** @@ -139,7 +139,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { // assertNotNull(roomFromBobPOV.powerLevels) // assertTrue(roomFromBobPOV.powerLevels.maySendMessage(bobSession.myUserId)) - return CryptoTestData(aliceRoomId, listOf(aliceSession, bobSession)) + return CryptoTestData(aliceRoomId, mutableListOf(aliceSession, bobSession)) } /** @@ -157,7 +157,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { // wait the initial sync SystemClock.sleep(1000) - return CryptoTestData(aliceRoomId, listOf(aliceSession, cryptoTestData.secondSession!!, samSession)) + return CryptoTestData(aliceRoomId, mutableListOf(aliceSession, cryptoTestData.secondSession!!, samSession)) } /** @@ -381,4 +381,31 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { } } } + + fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomId = mTestHelper.doSync { + aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it) + } + val room = aliceSession.getRoom(roomId)!! + + mTestHelper.runBlockingTest { + room.enableEncryption() + } + + val cryptoTestData = CryptoTestData(roomId, mutableListOf(aliceSession)) + for (index in 1 until numberOfMembers) { + mTestHelper + .createAccount("User_$index", defaultSessionParams) + .also { session -> mTestHelper.doSync { room.invite(session.myUserId, null, it) } } + .also { println("TEST -> " + it.myUserId + " invited") } + .also { session -> mTestHelper.doSync { session.joinRoom(room.roomId, null, emptyList(), it) } } + .also { println("TEST -> " + it.myUserId + " joined") } + .also { session -> cryptoTestData.sessions.add(session) } + } + + return cryptoTestData + } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt index a0271cb5b9..1a397c1d1c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.session.room.timeline +import android.os.SystemClock +import android.util.Log import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -29,11 +31,14 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper import java.util.concurrent.CountDownLatch +import kotlin.test.assertEquals @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) class TimelineWithManyMembersTest : InstrumentedTest { + private val NUMBER_OF_MEMBERS = 5 + private val commonTestHelper = CommonTestHelper(context()) private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) @@ -91,4 +96,42 @@ class TimelineWithManyMembersTest : InstrumentedTest { } samSession.stopSync() } + + /** + * Ensures when someone sends a message to a crowded room, everyone can decrypt the message. + */ + @Test + fun everyone_should_decrypt_message_in_a_crowded_room() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithManyMembers(NUMBER_OF_MEMBERS) + + val sessionForFirstMember = cryptoTestData.firstSession + val roomForFirstMember = sessionForFirstMember.getRoom(cryptoTestData.roomId)!! + + val firstMessage = "First messages from Alice" + commonTestHelper.sendTextMessage( + roomForFirstMember, + firstMessage, + 1) + + for (index in 1 until cryptoTestData.sessions.size) { + val session = cryptoTestData.sessions[index] + val roomForCurrentMember = session.getRoom(cryptoTestData.roomId)!! + val timelineForCurrentMember = roomForCurrentMember.createTimeline(null, TimelineSettings(30)) + timelineForCurrentMember.start() + + session.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + val decryptedMessage = snapshot.firstOrNull()?.root?.getClearContent()?.toModel()?.body + println("Decrypted Message: $decryptedMessage") + return@createEventListener decryptedMessage?.startsWith(firstMessage).orFalse() + } + timelineForCurrentMember.addListener(eventsListener) + commonTestHelper.await(lock) + } + session.stopSync() + } + } } From b263273c878806ba3f474380f438349c7f8b70ac Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 16 Dec 2020 13:45:26 +0300 Subject: [PATCH 04/57] Improve test with detailed CryptoError message. --- .../android/sdk/common/CommonTestHelper.kt | 4 ++-- .../android/sdk/common/CryptoTestHelper.kt | 2 +- .../timeline/TimelineWithManyMembersTest.kt | 23 ++++++++++++------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 0e7088a6a5..7dcf43ba0f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -119,7 +119,7 @@ class CommonTestHelper(context: Context) { * @param message the message to send * @param nbOfMessages the number of time the message will be sent */ - fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List { + fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List { val timeline = room.createTimeline(null, TimelineSettings(10)) val sentEvents = ArrayList(nbOfMessages) val latch = CountDownLatch(1) @@ -151,7 +151,7 @@ class CommonTestHelper(context: Context) { room.sendTextMessage(message + " #" + (i + 1)) } // Wait 3 second more per message - await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages) + await(latch, timeout = timeout + 3_000L * nbOfMessages) timeline.dispose() // Check that all events has been created diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 66fc1348d5..5d3407dde1 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -399,7 +399,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { for (index in 1 until numberOfMembers) { mTestHelper .createAccount("User_$index", defaultSessionParams) - .also { session -> mTestHelper.doSync { room.invite(session.myUserId, null, it) } } + .also { session -> mTestHelper.doSync(timeout = 600_000) { room.invite(session.myUserId, null, it) } } .also { println("TEST -> " + it.myUserId + " invited") } .also { session -> mTestHelper.doSync { session.joinRoom(room.roomId, null, emptyList(), it) } } .also { println("TEST -> " + it.myUserId + " joined") } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt index 1a397c1d1c..dacde121b3 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt @@ -16,8 +16,6 @@ package org.matrix.android.sdk.session.room.timeline -import android.os.SystemClock -import android.util.Log import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -31,7 +29,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper import java.util.concurrent.CountDownLatch -import kotlin.test.assertEquals +import kotlin.test.fail @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -111,7 +109,9 @@ class TimelineWithManyMembersTest : InstrumentedTest { commonTestHelper.sendTextMessage( roomForFirstMember, firstMessage, - 1) + 1, + 600_000 + ) for (index in 1 until cryptoTestData.sessions.size) { val session = cryptoTestData.sessions[index] @@ -124,12 +124,19 @@ class TimelineWithManyMembersTest : InstrumentedTest { run { val lock = CountDownLatch(1) val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - val decryptedMessage = snapshot.firstOrNull()?.root?.getClearContent()?.toModel()?.body - println("Decrypted Message: $decryptedMessage") - return@createEventListener decryptedMessage?.startsWith(firstMessage).orFalse() + snapshot + .find { it.isEncrypted() } + ?.let { + val body = it.root.getClearContent()?.toModel()?.body + if (body?.startsWith(firstMessage).orFalse()) { + return@createEventListener true + } else { + fail("User " + session.myUserId + " decrypted as " + body + " CryptoError: " + it.root.mCryptoError) + } + } ?: return@createEventListener false } timelineForCurrentMember.addListener(eventsListener) - commonTestHelper.await(lock) + commonTestHelper.await(lock, 600_000) } session.stopSync() } From 7b97981bb57ad7b651700ecacc878f4c1324967f Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 16 Dec 2020 15:45:50 +0300 Subject: [PATCH 05/57] Make sure to load all members in the room before sending the event. --- .../android/sdk/common/CommonTestHelper.kt | 10 ++-- .../timeline/TimelineWithManyMembersTest.kt | 58 +------------------ .../internal/crypto/tasks/SendEventTask.kt | 9 +++ 3 files changed, 16 insertions(+), 61 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 7dcf43ba0f..cb49ee8818 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -86,7 +86,7 @@ class CommonTestHelper(context: Context) { * * @param session the session to sync */ - fun syncSession(session: Session) { + fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis) { val lock = CountDownLatch(1) val job = GlobalScope.launch(Dispatchers.Main) { @@ -109,7 +109,7 @@ class CommonTestHelper(context: Context) { } GlobalScope.launch(Dispatchers.Main) { syncLiveData.observeForever(syncObserver) } - await(lock) + await(lock, timeout) } /** @@ -215,14 +215,14 @@ class CommonTestHelper(context: Context) { .getLoginFlow(hs, it) } - doSync { + doSync(timeout = 60_000) { matrix.authenticationService .getRegistrationWizard() .createAccount(userName, password, null, it) } // Perform dummy step - val registrationResult = doSync { + val registrationResult = doSync(timeout = 60_000) { matrix.authenticationService .getRegistrationWizard() .dummy(it) @@ -231,7 +231,7 @@ class CommonTestHelper(context: Context) { assertTrue(registrationResult is RegistrationResult.Success) val session = (registrationResult as RegistrationResult.Success).session if (sessionTestParams.withInitialSync) { - syncSession(session) + syncSession(session, 60_000) } return session diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt index dacde121b3..ca74acdee7 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt @@ -35,66 +35,11 @@ import kotlin.test.fail @FixMethodOrder(MethodSorters.JVM) class TimelineWithManyMembersTest : InstrumentedTest { - private val NUMBER_OF_MEMBERS = 5 + private val NUMBER_OF_MEMBERS = 6 private val commonTestHelper = CommonTestHelper(context()) private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - /** - * Ensures when someone sends a message to a crowded room, everyone can decrypt the message. - */ - @Test - fun everyoneShouldDecryptMessage3Members() { - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobAndSamInARoom() - - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession!! - val samSession = cryptoTestData.thirdSession!! - - val aliceRoomId = cryptoTestData.roomId - - aliceSession.cryptoService().setWarnOnUnknownDevices(false) - bobSession.cryptoService().setWarnOnUnknownDevices(false) - samSession.cryptoService().setWarnOnUnknownDevices(false) - - val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! - val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! - val roomFromSamPOV = samSession.getRoom(aliceRoomId)!! - - val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) - val samTimeline = roomFromSamPOV.createTimeline(null, TimelineSettings(30)) - bobTimeline.start() - samTimeline.start() - - val firstMessage = "First messages from Alice" - commonTestHelper.sendTextMessage( - roomFromAlicePOV, - firstMessage, - 1) - - bobSession.startSync(true) - run { - val lock = CountDownLatch(1) - val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - snapshot.firstOrNull()?.root?.getClearContent()?.toModel()?.body?.startsWith(firstMessage).orFalse() - } - bobTimeline.addListener(eventsListener) - commonTestHelper.await(lock) - } - bobSession.stopSync() - - samSession.startSync(true) - run { - val lock = CountDownLatch(1) - val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - snapshot.firstOrNull()?.root?.getClearContent()?.toModel()?.body?.startsWith(firstMessage).orFalse() - } - samTimeline.addListener(eventsListener) - commonTestHelper.await(lock) - } - samSession.stopSync() - } - /** * Ensures when someone sends a message to a crowded room, everyone can decrypt the message. */ @@ -129,6 +74,7 @@ class TimelineWithManyMembersTest : InstrumentedTest { ?.let { val body = it.root.getClearContent()?.toModel()?.body if (body?.startsWith(firstMessage).orFalse()) { + println("User " + session.myUserId + " decrypted as " + body) return@createEventListener true } else { fail("User " + session.myUserId + " decrypted as " + body + " CryptoError: " + it.root.mCryptoError) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index 8b739c4b64..5c8c7dfb25 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -20,6 +20,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.task.Task @@ -35,11 +36,19 @@ internal interface SendEventTask : Task { internal class DefaultSendEventTask @Inject constructor( private val localEchoRepository: LocalEchoRepository, private val encryptEventTask: DefaultEncryptEventTask, + private val loadRoomMembersTask: LoadRoomMembersTask, private val roomAPI: RoomAPI, private val eventBus: EventBus) : SendEventTask { override suspend fun execute(params: SendEventTask.Params): String { try { + // Make sure to load all members in the room before sending the event. + params.event.roomId + ?.takeIf { params.encrypt } + ?.let { roomId -> + loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) + } + val event = handleEncryption(params) val localId = event.eventId!! From 80396fcd391d195be206dedd914115fd6bc8722c Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 16 Dec 2020 15:46:14 +0300 Subject: [PATCH 06/57] Changelog added. --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 0fb7d72c13..9ab5276ff3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -57,6 +57,7 @@ Bugfix 🐛: - No known servers error is given when joining rooms on new Gitter bridge (#2516) - Show preview when sending attachment from the keyboard (#2440) - Do not compress GIFs (#1616, #1254) + - Wait for all room members to be known before sending a message to a e2e room (#2518) SDK API changes ⚠️: - StateService now exposes suspendable function instead of using MatrixCallback. From 938cd32ddd97a3608a1497d0cf83b97150a33a3a Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 17 Dec 2020 18:25:40 +0300 Subject: [PATCH 07/57] Do not load room members if there is an ongoing request. --- .../database/RealmSessionStoreMigration.kt | 18 ++++++++++++++- .../sdk/internal/database/model/RoomEntity.kt | 10 +++++++- .../model/RoomMembersLoadStatusType.java | 23 +++++++++++++++++++ .../room/membership/LoadRoomMembersTask.kt | 5 ++-- 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.java diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index b970ec60e2..fd922ef0e5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -21,6 +21,7 @@ import io.realm.RealmMigration import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields +import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import timber.log.Timber import javax.inject.Inject @@ -28,7 +29,7 @@ import javax.inject.Inject class RealmSessionStoreMigration @Inject constructor() : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 6L + const val SESSION_STORE_SCHEMA_VERSION = 7L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -40,6 +41,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 4) migrateTo5(realm) if (oldVersion <= 5) migrateTo6(realm) + if (oldVersion <= 6) migrateTo7(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -105,4 +107,18 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { .addField(PreviewUrlCacheEntityFields.MXC_URL, String::class.java) .addField(PreviewUrlCacheEntityFields.LAST_UPDATED_TIMESTAMP, Long::class.java) } + + private fun migrateTo7(realm: DynamicRealm) { + Timber.d("Step 6 -> 7") + realm.schema.get("RoomEntity") + ?.addField("membersLoadStatusStr", String::class.java) + ?.transform { obj -> + if (obj.getBoolean("areAllMembersLoaded")) { + obj.setString("membersLoadStatusStr", RoomMembersLoadStatusType.LOADED.name) + } else { + obj.setString("membersLoadStatusStr", RoomMembersLoadStatusType.NONE.name) + } + } + ?.removeField("areAllMembersLoaded") + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt index 9af1646a4c..a0a6060d36 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt @@ -24,7 +24,6 @@ import io.realm.annotations.PrimaryKey internal open class RoomEntity(@PrimaryKey var roomId: String = "", var chunks: RealmList = RealmList(), var sendingTimelineEvents: RealmList = RealmList(), - var areAllMembersLoaded: Boolean = false ) : RealmObject() { private var membershipStr: String = Membership.NONE.name @@ -36,5 +35,14 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "", membershipStr = value.name } + private var membersLoadStatusStr: String = RoomMembersLoadStatusType.NONE.name + var membersLoadStatus: RoomMembersLoadStatusType + get() { + return RoomMembersLoadStatusType.valueOf(membersLoadStatusStr) + } + set(value) { + membersLoadStatusStr = value.name + } + companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.java new file mode 100644 index 0000000000..d7063082bb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.database.model; + +public enum RoomMembersLoadStatusType { + NONE, + LOADING, + LOADED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt index 627f927ad8..69530c5c0e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt @@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.util.awaitTransaction import io.realm.Realm import io.realm.kotlin.createObject import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import javax.inject.Inject internal interface LoadRoomMembersTask : Task { @@ -84,14 +85,14 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( } roomMemberEventHandler.handle(realm, roomId, roomMemberEvent) } - roomEntity.areAllMembersLoaded = true + roomEntity.membersLoadStatus = RoomMembersLoadStatusType.LOADED roomSummaryUpdater.update(realm, roomId, updateMembers = true) } } private fun areAllMembersAlreadyLoaded(roomId: String): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { - RoomEntity.where(it, roomId).findFirst()?.areAllMembersLoaded ?: false + RoomEntity.where(it, roomId).findFirst()?.membersLoadStatus == RoomMembersLoadStatusType.LOADED } } } From 5d8f365520a0f48d94346d3e13b5fc431738e06c Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 17 Dec 2020 18:55:31 +0300 Subject: [PATCH 08/57] Load room members seamlessly when timeline is starting. --- .../sdk/internal/database/model/RoomEntity.kt | 2 +- .../session/room/timeline/DefaultTimeline.kt | 12 +++++++++++- .../session/room/timeline/DefaultTimelineService.kt | 7 +++++-- .../features/home/room/detail/RoomDetailViewModel.kt | 2 -- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt index a0a6060d36..3ff2532604 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt @@ -23,7 +23,7 @@ import io.realm.annotations.PrimaryKey internal open class RoomEntity(@PrimaryKey var roomId: String = "", var chunks: RealmList = RealmList(), - var sendingTimelineEvents: RealmList = RealmList(), + var sendingTimelineEvents: RealmList = RealmList() ) : RealmObject() { private var membershipStr: String = Membership.NONE.name diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 86b0497bd0..dd58529412 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -27,6 +27,7 @@ import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType @@ -53,6 +54,7 @@ import org.matrix.android.sdk.internal.database.query.filterEvents import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId +import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.Debouncer @@ -81,7 +83,8 @@ internal class DefaultTimeline( private val hiddenReadReceipts: TimelineHiddenReadReceipts, private val eventBus: EventBus, private val eventDecryptor: TimelineEventDecryptor, - private val realmSessionProvider: RealmSessionProvider + private val realmSessionProvider: RealmSessionProvider, + private val loadRoomMembersTask: LoadRoomMembersTask ) : Timeline, TimelineHiddenReadReceipts.Delegate { data class OnNewTimelineEvents(val roomId: String, val eventIds: List) @@ -184,6 +187,13 @@ internal class DefaultTimeline( if (settings.shouldHandleHiddenReadReceipts()) { hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) } + + loadRoomMembersTask + .configureWith(LoadRoomMembersTask.Params(roomId)) { + this.callback = NoOpMatrixCallback() + } + .executeBy(taskExecutor) + isReady.set(true) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 783aa53ddf..d02e906d00 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -39,6 +39,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.task.TaskExecutor internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, @@ -51,7 +52,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv private val paginationTask: PaginationTask, private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val timelineEventMapper: TimelineEventMapper, - private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, + private val loadRoomMembersTask: LoadRoomMembersTask ) : TimelineService { @AssistedInject.Factory @@ -73,7 +75,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv eventBus = eventBus, eventDecryptor = eventDecryptor, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, - realmSessionProvider = realmSessionProvider + realmSessionProvider = realmSessionProvider, + loadRoomMembersTask = loadRoomMembersTask ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index e4e7177e4f..1e6e7c9d14 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -31,7 +31,6 @@ import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider -import im.vector.app.core.utils.subscribeLogError import im.vector.app.features.call.WebRtcPeerConnectionManager import im.vector.app.features.command.CommandParser import im.vector.app.features.command.ParsedCommand @@ -168,7 +167,6 @@ class RoomDetailViewModel @AssistedInject constructor( observePowerLevel() room.getRoomSummaryLive() room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback()) - room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() // Inform the SDK that the room is displayed session.onRoomDisplayed(initialState.roomId) chatEffectManager.delegate = this From 42a5680374ef8fd8f71ee85b5a9c7c198be970ea Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 18 Dec 2020 12:27:51 +0300 Subject: [PATCH 09/57] Fix copyright. --- .../sdk/session/room/timeline/TimelineWithManyMembersTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt index ca74acdee7..6dd139390c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 7732bd47cebbcaadb53c84615a491ce271cf25db Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 18 Dec 2020 16:01:39 +0100 Subject: [PATCH 10/57] Update Changelog after release --- CHANGES.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9ab5276ff3..38868ed822 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ Improvements 🙌: - Bugfix 🐛: - - + - Wait for all room members to be known before sending a message to a e2e room (#2518) Translations 🗣: - @@ -57,7 +57,6 @@ Bugfix 🐛: - No known servers error is given when joining rooms on new Gitter bridge (#2516) - Show preview when sending attachment from the keyboard (#2440) - Do not compress GIFs (#1616, #1254) - - Wait for all room members to be known before sending a message to a e2e room (#2518) SDK API changes ⚠️: - StateService now exposes suspendable function instead of using MatrixCallback. From ff8a20801253969911f7fde8faba3265cca58cb2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 18 Dec 2020 16:04:46 +0100 Subject: [PATCH 11/57] Change to immutable list --- .../org/matrix/android/sdk/common/CryptoTestData.kt | 2 +- .../matrix/android/sdk/common/CryptoTestHelper.kt | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt index 693ab28da1..b6bedbd719 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.common import org.matrix.android.sdk.api.session.Session data class CryptoTestData(val roomId: String, - val sessions: MutableList = mutableListOf()) { + val sessions: List) { val firstSession: Session get() = sessions.first() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 5d3407dde1..f219986c49 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -73,7 +73,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { } } - return CryptoTestData(roomId, mutableListOf(aliceSession)) + return CryptoTestData(roomId, listOf(aliceSession)) } /** @@ -139,7 +139,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { // assertNotNull(roomFromBobPOV.powerLevels) // assertTrue(roomFromBobPOV.powerLevels.maySendMessage(bobSession.myUserId)) - return CryptoTestData(aliceRoomId, mutableListOf(aliceSession, bobSession)) + return CryptoTestData(aliceRoomId, listOf(aliceSession, bobSession)) } /** @@ -157,7 +157,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { // wait the initial sync SystemClock.sleep(1000) - return CryptoTestData(aliceRoomId, mutableListOf(aliceSession, cryptoTestData.secondSession!!, samSession)) + return CryptoTestData(aliceRoomId, listOf(aliceSession, cryptoTestData.secondSession!!, samSession)) } /** @@ -395,7 +395,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { room.enableEncryption() } - val cryptoTestData = CryptoTestData(roomId, mutableListOf(aliceSession)) + val sessions = mutableListOf(aliceSession) for (index in 1 until numberOfMembers) { mTestHelper .createAccount("User_$index", defaultSessionParams) @@ -403,9 +403,9 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { .also { println("TEST -> " + it.myUserId + " invited") } .also { session -> mTestHelper.doSync { session.joinRoom(room.roomId, null, emptyList(), it) } } .also { println("TEST -> " + it.myUserId + " joined") } - .also { session -> cryptoTestData.sessions.add(session) } + .also { session -> sessions.add(session) } } - return cryptoTestData + return CryptoTestData(roomId, sessions) } } From 00b16db7cc823e8ef127fbc3e43d250cb428d03f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 18 Dec 2020 16:06:30 +0100 Subject: [PATCH 12/57] Simplification of code --- .../matrix/android/sdk/common/CryptoTestHelper.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index f219986c49..3d5856fc64 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -397,13 +397,12 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { val sessions = mutableListOf(aliceSession) for (index in 1 until numberOfMembers) { - mTestHelper - .createAccount("User_$index", defaultSessionParams) - .also { session -> mTestHelper.doSync(timeout = 600_000) { room.invite(session.myUserId, null, it) } } - .also { println("TEST -> " + it.myUserId + " invited") } - .also { session -> mTestHelper.doSync { session.joinRoom(room.roomId, null, emptyList(), it) } } - .also { println("TEST -> " + it.myUserId + " joined") } - .also { session -> sessions.add(session) } + val session = mTestHelper.createAccount("User_$index", defaultSessionParams) + mTestHelper.doSync(timeout = 600_000) { room.invite(session.myUserId, null, it) } + println("TEST -> " + session.myUserId + " invited") + mTestHelper.doSync { session.joinRoom(room.roomId, null, emptyList(), it) } + println("TEST -> " + session.myUserId + " joined") + sessions.add(session) } return CryptoTestData(roomId, sessions) From 15597eb041899df40c582f2d9793fb9bf07d8845 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 18 Dec 2020 16:10:36 +0100 Subject: [PATCH 13/57] Rename .java to .kt --- ...oomMembersLoadStatusType.java => RoomMembersLoadStatusType.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/{RoomMembersLoadStatusType.java => RoomMembersLoadStatusType.kt} (100%) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.java rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.kt From abf763f454ac7e89f1d6fe314c112394749c7ac2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 18 Dec 2020 16:10:36 +0100 Subject: [PATCH 14/57] Convert to internal Kotlin class --- .../sdk/internal/database/model/RoomMembersLoadStatusType.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.kt index d7063082bb..79fe17253b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.database.model; +package org.matrix.android.sdk.internal.database.model -public enum RoomMembersLoadStatusType { +internal enum class RoomMembersLoadStatusType { NONE, LOADING, LOADED From b0ba62aa312c9847277f47489b7e892aa34cba3f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 18 Dec 2020 16:12:01 +0100 Subject: [PATCH 15/57] Use const --- .../sdk/internal/database/RealmSessionStoreMigration.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index fd922ef0e5..57002b5a60 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -21,6 +21,7 @@ import io.realm.RealmMigration import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields +import org.matrix.android.sdk.internal.database.model.RoomEntityFields import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import timber.log.Timber @@ -111,7 +112,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { private fun migrateTo7(realm: DynamicRealm) { Timber.d("Step 6 -> 7") realm.schema.get("RoomEntity") - ?.addField("membersLoadStatusStr", String::class.java) + ?.addField(RoomEntityFields.MEMBERS_LOAD_STATUS_STR, String::class.java) ?.transform { obj -> if (obj.getBoolean("areAllMembersLoaded")) { obj.setString("membersLoadStatusStr", RoomMembersLoadStatusType.LOADED.name) From ca4b91a98f3086a4ce167d9942023233e039aa0c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 18 Dec 2020 16:38:27 +0100 Subject: [PATCH 16/57] Use the new RoomMembersLoadStatusType.LOADING value --- .../room/membership/LoadRoomMembersTask.kt | 48 +++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt index 69530c5c0e..55dd747fdb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt @@ -17,12 +17,19 @@ package org.matrix.android.sdk.internal.session.room.membership import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import io.realm.kotlin.createObject +import kotlinx.coroutines.TimeoutCancellationException +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where @@ -33,10 +40,7 @@ import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction -import io.realm.Realm -import io.realm.kotlin.createObject -import org.greenrobot.eventbus.EventBus -import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType +import java.util.concurrent.TimeUnit import javax.inject.Inject internal interface LoadRoomMembersTask : Task { @@ -57,9 +61,33 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( ) : LoadRoomMembersTask { override suspend fun execute(params: LoadRoomMembersTask.Params) { - if (areAllMembersAlreadyLoaded(params.roomId)) { - return + when (getRoomMembersLoadStatus(params.roomId)) { + RoomMembersLoadStatusType.NONE -> doRequest(params) + RoomMembersLoadStatusType.LOADING -> waitPreviousRequestToFinish(params) + RoomMembersLoadStatusType.LOADED -> Unit } + } + + private suspend fun waitPreviousRequestToFinish(params: LoadRoomMembersTask.Params) { + try { + awaitNotEmptyResult(monarchy.realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, params.roomId) + .equalTo(RoomEntityFields.MEMBERS_LOAD_STATUS_STR, RoomMembersLoadStatusType.LOADED.name) + } + } catch (exception: TimeoutCancellationException) { + // Timeout, do the request anyway (?) + doRequest(params) + } + } + + private suspend fun doRequest(params: LoadRoomMembersTask.Params) { + monarchy.awaitTransaction { realm -> + val roomEntity = RoomEntity.where(realm, params.roomId).findFirst() + ?: realm.createObject(params.roomId) + roomEntity.membersLoadStatus = RoomMembersLoadStatusType.LOADING + } + val lastToken = syncTokenStore.getLastToken() val response = executeRequest(eventBus) { apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) @@ -90,9 +118,11 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( } } - private fun areAllMembersAlreadyLoaded(roomId: String): Boolean { - return Realm.getInstance(monarchy.realmConfiguration).use { - RoomEntity.where(it, roomId).findFirst()?.membersLoadStatus == RoomMembersLoadStatusType.LOADED + private fun getRoomMembersLoadStatus(roomId: String): RoomMembersLoadStatusType { + var result: RoomMembersLoadStatusType? + Realm.getInstance(monarchy.realmConfiguration).use { + result = RoomEntity.where(it, roomId).findFirst()?.membersLoadStatus } + return result ?: RoomMembersLoadStatusType.NONE } } From 3d291c04c981e6b70336dfe35ea1836190e5bf7e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 18 Dec 2020 16:53:26 +0100 Subject: [PATCH 17/57] const -> companion --- .../sdk/session/room/timeline/TimelineWithManyMembersTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt index 6dd139390c..ff07cf1d1d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt @@ -35,7 +35,9 @@ import kotlin.test.fail @FixMethodOrder(MethodSorters.JVM) class TimelineWithManyMembersTest : InstrumentedTest { - private val NUMBER_OF_MEMBERS = 6 + companion object { + private const val NUMBER_OF_MEMBERS = 6 + } private val commonTestHelper = CommonTestHelper(context()) private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) From 13938f2ab3eb02e55e51ee9f924c7a52d047fd4e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 21 Dec 2020 11:18:25 +0100 Subject: [PATCH 18/57] Let the Matrix SDK compute the SSO url --- .../sdk/api/auth/AuthenticationService.kt | 5 +++ .../matrix/android/sdk/api/auth/Constants.kt | 6 +-- .../android/sdk/api/util/UrlExtensions.kt | 37 +++++++++++++++++++ .../auth/DefaultAuthenticationService.kt | 26 +++++++++++++ .../app/core/extensions/UrlExtensions.kt | 20 ---------- .../login/AbstractSSOLoginFragment.kt | 7 +++- .../app/features/login/LoginActivity.kt | 3 ++ .../app/features/login/LoginFragment.kt | 7 +++- .../LoginSignUpSignInSelectionFragment.kt | 15 ++++++-- .../app/features/login/LoginViewModel.kt | 4 ++ .../app/features/login/LoginViewState.kt | 27 -------------- .../app/features/login/LoginWebFragment.kt | 2 +- 12 files changed, 103 insertions(+), 56 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/UrlExtensions.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt index 360b955869..3314b47ce9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -41,6 +41,11 @@ interface AuthenticationService { */ fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback): Cancelable + /** + * Get a SSO url + */ + fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? + /** * Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt index 7d18aba627..d832caefde 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt @@ -32,7 +32,7 @@ const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/" * Path to use when the client want to connect using SSO * Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login */ -const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect" -const val MSC2858_SSO_REDIRECT_PATH = "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect" +internal const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect" +internal const val MSC2858_SSO_REDIRECT_PATH = "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect" -const val SSO_REDIRECT_URL_PARAM = "redirectUrl" +internal const val SSO_REDIRECT_URL_PARAM = "redirectUrl" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/UrlExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/UrlExtensions.kt new file mode 100644 index 0000000000..beaff2bdda --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/UrlExtensions.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 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 org.matrix.android.sdk.api.util + +import java.net.URLEncoder + +/** + * Append param and value to a Url, using "?" or "&". Value parameter will be encoded + * Return this for chaining purpose + */ +fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder { + if (contains("?")) { + append("&") + } else { + append("?") + } + + append(param) + append("=") + append(URLEncoder.encode(value, "utf-8")) + + return this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 55f053de8d..51f6b6c155 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -23,6 +23,9 @@ import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.MSC2858_SSO_REDIRECT_PATH +import org.matrix.android.sdk.api.auth.SSO_REDIRECT_PATH +import org.matrix.android.sdk.api.auth.SSO_REDIRECT_URL_PARAM import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.LoginFlowResult @@ -34,6 +37,7 @@ import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.api.util.appendParamToUrl import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse import org.matrix.android.sdk.internal.auth.data.RiotConfig @@ -99,6 +103,28 @@ internal class DefaultAuthenticationService @Inject constructor( } } + override fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { + return pendingSessionData?.let { safePendingSessionData -> + val homeServerUrl = safePendingSessionData.homeServerConnectionConfig.homeServerUri.toString() + + buildString { + append(homeServerUrl.trim { it == '/' }) + if (providerId != null) { + append(MSC2858_SSO_REDIRECT_PATH) + append("/$providerId") + } else { + append(SSO_REDIRECT_PATH) + } + // Set a redirect url we will intercept later + appendParamToUrl(SSO_REDIRECT_URL_PARAM, redirectUrl) + deviceId?.takeIf { it.isNotBlank() }?.let { + // But https://github.com/matrix-org/synapse/issues/5755 + appendParamToUrl("device_id", it) + } + } + } + } + override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable { pendingSessionData = null diff --git a/vector/src/main/java/im/vector/app/core/extensions/UrlExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/UrlExtensions.kt index 38977d33ba..5037f78445 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/UrlExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/UrlExtensions.kt @@ -16,26 +16,6 @@ package im.vector.app.core.extensions -import java.net.URLEncoder - -/** - * Append param and value to a Url, using "?" or "&". Value parameter will be encoded - * Return this for chaining purpose - */ -fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder { - if (contains("?")) { - append("&") - } else { - append("?") - } - - append(param) - append("=") - append(URLEncoder.encode(value, "utf-8")) - - return this -} - /** * Ex: "https://matrix.org/" -> "matrix.org" */ diff --git a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt index c20f4ddd23..3fc5037ae7 100644 --- a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt @@ -87,7 +87,12 @@ abstract class AbstractSSOLoginFragment : AbstractLoginFragment withState(loginViewModel) { state -> if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) { // in this case we can prefetch (not other cases for privacy concerns) - prefetchUrl(state.getSsoUrl(null)) + loginViewModel.getSsoUrl( + redirectUrl = LoginActivity.VECTOR_REDIRECT_URL, + deviceId = state.deviceId, + providerId = null + ) + ?.let { prefetchUrl(it) } } } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 503b6d74a6..803fd38983 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -360,6 +360,9 @@ open class LoginActivity : VectorBaseActivity(), ToolbarCo private const val EXTRA_CONFIG = "EXTRA_CONFIG" + // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string + const val VECTOR_REDIRECT_URL = "element://connect" + fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { return Intent(context, LoginActivity::class.java).apply { putExtra(EXTRA_CONFIG, loginConfig) diff --git a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt index c396e61b1a..3b22e0f206 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt @@ -193,7 +193,12 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment if (state.loginMode is LoginMode.Sso) { - openInCustomTab(state.getSsoUrl(null)) + loginViewModel.getSsoUrl( + redirectUrl = LoginActivity.VECTOR_REDIRECT_URL, + deviceId = state.deviceId, + providerId = null + ) + ?.let { openInCustomTab(it) } } else { loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index 0a6dbcaae2..ab79c6ae48 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -818,4 +818,8 @@ class LoginViewModel @AssistedInject constructor( fun getInitialHomeServerUrl(): String? { return loginConfig?.homeServerUrl } + + fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { + return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId) + } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt index 5254abf1d9..37ac89794f 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt @@ -22,10 +22,6 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.PersistState import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import im.vector.app.core.extensions.appendParamToUrl -import org.matrix.android.sdk.api.auth.MSC2858_SSO_REDIRECT_PATH -import org.matrix.android.sdk.api.auth.SSO_REDIRECT_PATH -import org.matrix.android.sdk.api.auth.SSO_REDIRECT_URL_PARAM data class LoginViewState( val asyncLoginAction: Async = Uninitialized, @@ -69,27 +65,4 @@ data class LoginViewState( fun isUserLogged(): Boolean { return asyncLoginAction is Success } - - fun getSsoUrl(providerId: String?): String { - return buildString { - append(homeServerUrl?.trim { it == '/' }) - if (providerId != null) { - append(MSC2858_SSO_REDIRECT_PATH) - append("/$providerId") - } else { - append(SSO_REDIRECT_PATH) - } - // Set a redirect url we will intercept later - appendParamToUrl(SSO_REDIRECT_URL_PARAM, VECTOR_REDIRECT_URL) - deviceId?.takeIf { it.isNotBlank() }?.let { - // But https://github.com/matrix-org/synapse/issues/5755 - appendParamToUrl("device_id", it) - } - } - } - - companion object { - // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string - private const val VECTOR_REDIRECT_URL = "element://connect" - } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt index acf4f706c5..7ba6626604 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt @@ -33,7 +33,6 @@ import android.webkit.WebViewClient import androidx.appcompat.app.AlertDialog import com.airbnb.mvrx.activityViewModel import im.vector.app.R -import im.vector.app.core.extensions.appendParamToUrl import im.vector.app.core.utils.AssetReader import im.vector.app.databinding.FragmentLoginWebBinding import im.vector.app.features.signout.soft.SoftLogoutAction @@ -42,6 +41,7 @@ import im.vector.app.features.signout.soft.SoftLogoutViewModel import org.matrix.android.sdk.api.auth.LOGIN_FALLBACK_PATH import org.matrix.android.sdk.api.auth.REGISTER_FALLBACK_PATH import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.util.appendParamToUrl import org.matrix.android.sdk.internal.di.MoshiProvider import timber.log.Timber import java.net.URLDecoder From 36a553a8868c57459994849ad25b9d8c8230b94e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 21 Dec 2020 12:08:49 +0100 Subject: [PATCH 19/57] Let the Matrix SDK compute the Fallback urls --- .../sdk/api/auth/AuthenticationService.kt | 5 ++ .../matrix/android/sdk/api/auth/Constants.kt | 4 +- .../auth/DefaultAuthenticationService.kt | 50 ++++++++++++++----- .../app/features/login/LoginViewModel.kt | 4 ++ .../app/features/login/LoginWebFragment.kt | 17 +------ 5 files changed, 50 insertions(+), 30 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt index 3314b47ce9..bf21941e0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -46,6 +46,11 @@ interface AuthenticationService { */ fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? + /** + * Get the sign in or sign up fallback URL + */ + fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? + /** * Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt index d832caefde..35b4a0a8d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt @@ -20,13 +20,13 @@ package org.matrix.android.sdk.api.auth * Path to use when the client does not supported any or all login flows * Ref: https://matrix.org/docs/spec/client_server/latest#login-fallback */ -const val LOGIN_FALLBACK_PATH = "/_matrix/static/client/login/" +internal const val LOGIN_FALLBACK_PATH = "/_matrix/static/client/login/" /** * Path to use when the client does not supported any or all registration flows * Not documented */ -const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/" +internal const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/" /** * Path to use when the client want to connect using SSO diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 51f6b6c155..0996011ed0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -23,7 +23,9 @@ import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.LOGIN_FALLBACK_PATH import org.matrix.android.sdk.api.auth.MSC2858_SSO_REDIRECT_PATH +import org.matrix.android.sdk.api.auth.REGISTER_FALLBACK_PATH import org.matrix.android.sdk.api.auth.SSO_REDIRECT_PATH import org.matrix.android.sdk.api.auth.SSO_REDIRECT_URL_PARAM import org.matrix.android.sdk.api.auth.data.Credentials @@ -104,27 +106,51 @@ internal class DefaultAuthenticationService @Inject constructor( } override fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { - return pendingSessionData?.let { safePendingSessionData -> - val homeServerUrl = safePendingSessionData.homeServerConnectionConfig.homeServerUri.toString() + val homeServerUrlBase = getHomeServerUrlBase() ?: return null - buildString { - append(homeServerUrl.trim { it == '/' }) - if (providerId != null) { - append(MSC2858_SSO_REDIRECT_PATH) - append("/$providerId") - } else { - append(SSO_REDIRECT_PATH) - } - // Set a redirect url we will intercept later - appendParamToUrl(SSO_REDIRECT_URL_PARAM, redirectUrl) + return buildString { + append(homeServerUrlBase) + if (providerId != null) { + append(MSC2858_SSO_REDIRECT_PATH) + append("/$providerId") + } else { + append(SSO_REDIRECT_PATH) + } + // Set the redirect url + appendParamToUrl(SSO_REDIRECT_URL_PARAM, redirectUrl) + deviceId?.takeIf { it.isNotBlank() }?.let { + // But https://github.com/matrix-org/synapse/issues/5755 + appendParamToUrl("device_id", it) + } + } + } + + override fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? { + val homeServerUrlBase = getHomeServerUrlBase() ?: return null + + return buildString { + append(homeServerUrlBase) + if (forSignIn) { + append(LOGIN_FALLBACK_PATH) deviceId?.takeIf { it.isNotBlank() }?.let { // But https://github.com/matrix-org/synapse/issues/5755 appendParamToUrl("device_id", it) } + } else { + // For sign up + append(REGISTER_FALLBACK_PATH) } } } + private fun getHomeServerUrlBase(): String? { + return pendingSessionData + ?.homeServerConnectionConfig + ?.homeServerUri + ?.toString() + ?.trim { it == '/' } + } + override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable { pendingSessionData = null diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index ab79c6ae48..666bd21add 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -822,4 +822,8 @@ class LoginViewModel @AssistedInject constructor( fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId) } + + fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? { + return authenticationService.getFallbackUrl(forSignIn, deviceId) + } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt index 7ba6626604..4b03c93321 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt @@ -38,10 +38,7 @@ import im.vector.app.databinding.FragmentLoginWebBinding import im.vector.app.features.signout.soft.SoftLogoutAction import im.vector.app.features.signout.soft.SoftLogoutViewModel -import org.matrix.android.sdk.api.auth.LOGIN_FALLBACK_PATH -import org.matrix.android.sdk.api.auth.REGISTER_FALLBACK_PATH import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.util.appendParamToUrl import org.matrix.android.sdk.internal.di.MoshiProvider import timber.log.Timber import java.net.URLDecoder @@ -119,19 +116,7 @@ class LoginWebFragment @Inject constructor( } private fun launchWebView(state: LoginViewState) { - val url = buildString { - append(state.homeServerUrl?.trim { it == '/' }) - if (state.signMode == SignMode.SignIn) { - append(LOGIN_FALLBACK_PATH) - state.deviceId?.takeIf { it.isNotBlank() }?.let { - // But https://github.com/matrix-org/synapse/issues/5755 - appendParamToUrl("device_id", it) - } - } else { - // MODE_REGISTER - append(REGISTER_FALLBACK_PATH) - } - } + val url = loginViewModel.getFallbackUrl(state.signMode == SignMode.SignIn, state.deviceId) ?: return views.loginWebWebView.loadUrl(url) From 6c4836e27e8ee05cac09a85e9e81f3b5db4e2b1f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 21 Dec 2020 12:11:19 +0100 Subject: [PATCH 20/57] Move file to internal --- .../matrix/android/sdk/{api => internal}/auth/Constants.kt | 2 +- .../sdk/internal/auth/DefaultAuthenticationService.kt | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{api => internal}/auth/Constants.kt (96%) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt similarity index 96% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt index 35b4a0a8d6..642279cc27 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.auth +package org.matrix.android.sdk.internal.auth /** * Path to use when the client does not supported any or all login flows diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 0996011ed0..c99e9bd81c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -23,11 +23,6 @@ import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.AuthenticationService -import org.matrix.android.sdk.api.auth.LOGIN_FALLBACK_PATH -import org.matrix.android.sdk.api.auth.MSC2858_SSO_REDIRECT_PATH -import org.matrix.android.sdk.api.auth.REGISTER_FALLBACK_PATH -import org.matrix.android.sdk.api.auth.SSO_REDIRECT_PATH -import org.matrix.android.sdk.api.auth.SSO_REDIRECT_URL_PARAM import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.LoginFlowResult From 629488bbe69ce617f5528120f2ed929c371362d5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 22 Dec 2020 11:58:38 +0100 Subject: [PATCH 21/57] VoIP: start introducing switch call --- .../im/vector/app/core/di/ViewModelModule.kt | 6 +- .../vector/app/core/services/CallService.kt | 83 +++++++++++++------ .../app/core/ui/views/ActiveCallView.kt | 20 +++++ .../app/core/ui/views/ActiveCallViewHolder.kt | 7 +- ...Model.kt => SharedCurrentCallViewModel.kt} | 24 ++++-- .../app/features/call/VectorCallActivity.kt | 4 - .../app/features/call/VectorCallViewModel.kt | 10 +-- .../app/features/call/webrtc/WebRtcCall.kt | 10 +++ .../features/call/webrtc/WebRtcCallManager.kt | 65 +++++++++------ .../app/features/home/HomeDetailFragment.kt | 15 ++-- .../home/room/detail/RoomDetailFragment.kt | 21 ++--- .../home/room/detail/RoomDetailViewModel.kt | 2 +- .../notifications/NotificationUtils.kt | 1 + .../main/res/layout/view_active_call_view.xml | 9 +- vector/src/main/res/values/strings.xml | 6 ++ 15 files changed, 183 insertions(+), 100 deletions(-) rename vector/src/main/java/im/vector/app/features/call/{SharedActiveCallViewModel.kt => SharedCurrentCallViewModel.kt} (71%) diff --git a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt index bed2e0b850..9d5c0d5491 100644 --- a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt @@ -22,7 +22,7 @@ import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import im.vector.app.core.platform.ConfigurationViewModel -import im.vector.app.features.call.SharedActiveCallViewModel +import im.vector.app.features.call.SharedCurrentCallViewModel import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel @@ -85,8 +85,8 @@ interface ViewModelModule { @Binds @IntoMap - @ViewModelKey(SharedActiveCallViewModel::class) - fun bindSharedActiveCallViewModel(viewModel: SharedActiveCallViewModel): ViewModel + @ViewModelKey(SharedCurrentCallViewModel::class) + fun bindSharedActiveCallViewModel(viewModel: SharedCurrentCallViewModel): ViewModel @Binds @IntoMap diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index a5a59dc0ba..efdec1c251 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -22,6 +22,7 @@ import android.content.Intent import android.os.Binder import android.support.v4.media.session.MediaSessionCompat import android.view.KeyEvent +import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.media.session.MediaButtonReceiver import com.airbnb.mvrx.MvRx @@ -46,7 +47,9 @@ import timber.log.Timber class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener { private val connections = mutableMapOf() + private val knownCalls = mutableSetOf() + private lateinit var notificationManager: NotificationManagerCompat private lateinit var notificationUtils: NotificationUtils private lateinit var callManager: WebRtcCallManager private lateinit var avatarRenderer: AvatarRenderer @@ -74,6 +77,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe override fun onCreate() { super.onCreate() + notificationManager = NotificationManagerCompat.from(this) notificationUtils = vectorComponent().notificationUtils() callManager = vectorComponent().webRtcCallManager() avatarRenderer = vectorComponent().avatarRenderer() @@ -130,7 +134,6 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe callRingPlayerOutgoing?.stop() displayCallInProgressNotification(intent) } - ACTION_NO_ACTIVE_CALL -> hideCallNotifications() ACTION_CALL_CONNECTING -> { // lower notification priority displayCallInProgressNotification(intent) @@ -138,6 +141,9 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe callRingPlayerIncoming?.stop() callRingPlayerOutgoing?.stop() } + ACTION_CALL_TERMINATED -> { + handleCallTerminated(intent) + } ACTION_ONGOING_CALL_BG -> { // there is an ongoing call but call activity is in background displayCallOnGoingInBackground(intent) @@ -166,11 +172,15 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe Timber.v("## VOIP displayIncomingCallNotification $intent") val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" val call = callManager.getCallById(callId) ?: return + if (knownCalls.contains(callId)) { + Timber.v("Call already notified $callId$") + return + } val isVideoCall = call.mxCall.isVideoCall val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false) val opponentMatrixItem = getOpponentMatrixItem(call) Timber.v("displayIncomingCallNotification : display the dedicated notification") - val incomingCallAlert = IncomingCallAlert(INCOMING_CALL_ALERT_UID, + val incomingCallAlert = IncomingCallAlert(callId, shouldBeDisplayedIn = { activity -> if (activity is RoomDetailActivity) { call.roomId != activity.currentRoomId @@ -195,7 +205,27 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId, fromBg = fromBg ) - startForeground(NOTIFICATION_ID, notification) + if (knownCalls.isEmpty()) { + startForeground(callId.hashCode(), notification) + } else { + notificationManager.notify(callId.hashCode(), notification) + } + knownCalls.add(callId) + } + + private fun handleCallTerminated(intent: Intent) { + val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" + if (!knownCalls.remove(callId)) { + Timber.v("Call terminated for unknown call $callId$") + return + } + val notification = notificationUtils.buildCallEndedNotification() + notificationManager.notify(callId.hashCode(), notification) + alertManager.cancelAlert(callId) + if (knownCalls.isEmpty()) { + mediaSession?.isActive = false + myStopSelf() + } } private fun showCallScreen(call: WebRtcCall, mode: String) { @@ -210,13 +240,22 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe private fun displayOutgoingRingingCallNotification(intent: Intent) { val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return val call = callManager.getCallById(callId) ?: return + if (knownCalls.contains(callId)) { + Timber.v("Call already notified $callId$") + return + } val opponentMatrixItem = getOpponentMatrixItem(call) Timber.v("displayOutgoingCallNotification : display the dedicated notification") val notification = notificationUtils.buildOutgoingRingingCallNotification( mxCall = call.mxCall, title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId ) - startForeground(NOTIFICATION_ID, notification) + if (knownCalls.isEmpty()) { + startForeground(callId.hashCode(), notification) + } else { + notificationManager.notify(callId.hashCode(), notification) + } + knownCalls.add(callId) } /** @@ -226,14 +265,17 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe Timber.v("## VOIP displayCallInProgressNotification") val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" val call = callManager.getCallById(callId) ?: return + if (!knownCalls.contains(callId)) { + Timber.v("Call in progress for unknown call $callId$") + return + } val opponentMatrixItem = getOpponentMatrixItem(call) - alertManager.cancelAlert(INCOMING_CALL_ALERT_UID) + alertManager.cancelAlert(callId) val notification = notificationUtils.buildPendingCallNotification( mxCall = call.mxCall, title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId ) - startForeground(NOTIFICATION_ID, notification) - // mCallIdInProgress = callId + notificationManager.notify(callId.hashCode(), notification) } /** @@ -243,27 +285,17 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe Timber.v("## VOIP displayCallInProgressNotification") val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return val call = callManager.getCallById(callId) ?: return + if (!knownCalls.contains(callId)) { + Timber.v("Call in in background for unknown call $callId$") + return + } val opponentMatrixItem = getOpponentMatrixItem(call) val notification = notificationUtils.buildPendingCallNotification( mxCall = call.mxCall, title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId, fromBg = true) - startForeground(NOTIFICATION_ID, notification) - // mCallIdInProgress = callId - } - - /** - * Hide the permanent call notifications - */ - private fun hideCallNotifications() { - val notification = notificationUtils.buildCallEndedNotification() - alertManager.cancelAlert(INCOMING_CALL_ALERT_UID) - mediaSession?.isActive = false - // It's mandatory to startForeground to avoid crash - startForeground(NOTIFICATION_ID, notification) - - myStopSelf() + notificationManager.notify(callId.hashCode(), notification) } fun addConnection(callConnection: CallConnection) { @@ -277,12 +309,12 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe companion object { private const val NOTIFICATION_ID = 6480 - private const val INCOMING_CALL_ALERT_UID = "INCOMING_CALL_ALERT_UID" private const val ACTION_INCOMING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_INCOMING_RINGING_CALL" private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_OUTGOING_RINGING_CALL" private const val ACTION_CALL_CONNECTING = "im.vector.app.core.services.CallService.ACTION_CALL_CONNECTING" private const val ACTION_ONGOING_CALL = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL" private const val ACTION_ONGOING_CALL_BG = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL_BG" + private const val ACTION_CALL_TERMINATED = "im.vector.app.core.services.CallService.ACTION_CALL_TERMINATED" private const val ACTION_NO_ACTIVE_CALL = "im.vector.app.core.services.CallService.NO_ACTIVE_CALL" // private const val ACTION_ACTIVITY_VISIBLE = "im.vector.app.core.services.CallService.ACTION_ACTIVITY_VISIBLE" // private const val ACTION_STOP_RINGING = "im.vector.app.core.services.CallService.ACTION_STOP_RINGING" @@ -335,10 +367,11 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe ContextCompat.startForegroundService(context, intent) } - fun onNoActiveCall(context: Context) { + fun onCallTerminated(context: Context, callId: String) { val intent = Intent(context, CallService::class.java) .apply { - action = ACTION_NO_ACTIVE_CALL + action = ACTION_CALL_TERMINATED + putExtra(EXTRA_CALL_ID, callId) } ContextCompat.startForegroundService(context, intent) } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt index 19d1fbb6f6..fd3159c2e0 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt @@ -20,7 +20,10 @@ import android.content.Context import android.util.AttributeSet import android.widget.RelativeLayout import im.vector.app.R +import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.themes.ThemeUtils +import kotlinx.android.synthetic.main.view_active_call_view.view.* +import org.matrix.android.sdk.api.session.call.CallState class ActiveCallView @JvmOverloads constructor( context: Context, @@ -43,4 +46,21 @@ class ActiveCallView @JvmOverloads constructor( setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary)) setOnClickListener { callback?.onTapToReturnToCall() } } + + fun render(calls: List) { + if (calls.size == 1) { + activeCallInfo.setText(R.string.call_active_call) + } else if (calls.size == 2) { + val activeCall = calls.firstOrNull { + it.mxCall.state is CallState.Connected && !it.isLocalOnHold() + } + if (activeCall == null) { + activeCallInfo.setText(R.string.call_two_paused_calls) + } else { + activeCallInfo.setText(R.string.call_one_active_one_paused_call) + } + } else { + visibility = GONE + } + } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt b/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt index 193a4b2387..fdf3b99986 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt @@ -16,7 +16,6 @@ package im.vector.app.core.ui.views -import android.view.View import androidx.cardview.widget.CardView import androidx.core.view.isVisible import im.vector.app.core.utils.DebouncedClickListener @@ -35,13 +34,14 @@ class ActiveCallViewHolder { private var activeCallPipInitialized = false - fun updateCall(activeCall: WebRtcCall?) { + fun updateCall(activeCall: WebRtcCall?, calls: List) { this.activeCall = activeCall val hasActiveCall = activeCall?.mxCall?.state is CallState.Connected if (hasActiveCall) { val isVideoCall = activeCall?.mxCall?.isVideoCall == true if (isVideoCall) initIfNeeded() activeCallView?.isVisible = !isVideoCall + activeCallView?.render(calls) pipWrapper?.isVisible = isVideoCall activeCallPiP?.isVisible = isVideoCall activeCallPiP?.let { @@ -74,10 +74,9 @@ class ActiveCallViewHolder { this.activeCallPiP = activeCallPiP this.activeCallView = activeCallView this.pipWrapper = pipWrapper - this.activeCallView?.callback = interactionListener pipWrapper.setOnClickListener( - DebouncedClickListener(View.OnClickListener { _ -> + DebouncedClickListener({ _ -> interactionListener.onTapToReturnToCall() }) ) diff --git a/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/SharedCurrentCallViewModel.kt similarity index 71% rename from vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt rename to vector/src/main/java/im/vector/app/features/call/SharedCurrentCallViewModel.kt index e35ed3e87a..d823fa6d5b 100644 --- a/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/SharedCurrentCallViewModel.kt @@ -23,36 +23,42 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import org.matrix.android.sdk.api.session.call.MxCall import javax.inject.Inject -class SharedActiveCallViewModel @Inject constructor( +class SharedCurrentCallViewModel @Inject constructor( private val callManager: WebRtcCallManager ) : ViewModel() { - val activeCall: MutableLiveData = MutableLiveData() + val currentCall: MutableLiveData = MutableLiveData() val callStateListener = object : WebRtcCall.Listener { override fun onStateUpdate(call: MxCall) { - if (activeCall.value?.callId == call.callId) { - activeCall.postValue(callManager.getCallById(call.callId)) - } + //post it-self + currentCall.postValue(currentCall.value) } + + override fun onHoldUnhold() { + super.onHoldUnhold() + //post it-self + currentCall.postValue(currentCall.value) + } + } private val listener = object : WebRtcCallManager.CurrentCallListener { override fun onCurrentCallChange(call: WebRtcCall?) { - activeCall.value?.mxCall?.removeListener(callStateListener) - activeCall.postValue(call) + currentCall.value?.mxCall?.removeListener(callStateListener) + currentCall.postValue(call) call?.addListener(callStateListener) } } init { - activeCall.postValue(callManager.currentCall) + currentCall.postValue(callManager.currentCall) callManager.addCurrentCallListener(listener) } override fun onCleared() { - activeCall.value?.removeListener(callStateListener) + currentCall.value?.removeListener(callStateListener) callManager.removeCurrentCallListener(listener) super.onCleared() } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index c9a078e260..2e5b54ae9a 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -106,7 +106,6 @@ class VectorCallActivity : VectorBaseActivity(), CallContro callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!! } else { Timber.e("## VOIP missing callArgs for VectorCall Activity") - CallService.onNoActiveCall(this) finish() } @@ -153,8 +152,6 @@ class VectorCallActivity : VectorBaseActivity(), CallContro private fun renderState(state: VectorCallViewState) { Timber.v("## VOIP renderState call $state") if (state.callState is Fail) { - // be sure to clear notification - CallService.onNoActiveCall(this) finish() return } @@ -295,7 +292,6 @@ class VectorCallActivity : VectorBaseActivity(), CallContro Timber.v("## VOIP handleViewEvents $event") when (event) { VectorCallViewEvents.DismissNoCall -> { - CallService.onNoActiveCall(this) finish() } is VectorCallViewEvents.ConnectionTimeout -> { diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index e1a01bbaa3..ecea4f1cb1 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -117,14 +117,6 @@ class VectorCallViewModel @AssistedInject constructor( private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { - override fun onCurrentCallChange(call: WebRtcCall?) { - // we need to check the state - if (call == null) { - // we should dismiss, e.g handled by other session? - _viewEvents.post(VectorCallViewEvents.DismissNoCall) - } - } - override fun onAudioDevicesChange() { val currentSoundDevice = callManager.callAudioManager.getCurrentSoundDevice() if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { @@ -163,6 +155,8 @@ class VectorCallViewModel @AssistedInject constructor( callState = Success(webRtcCall.mxCall.state), otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized, soundDevice = currentSoundDevice, + isLocalOnHold = webRtcCall.isLocalOnHold(), + isRemoteOnHold = webRtcCall.remoteOnHold, availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(), isFrontCamera = call?.currentCameraType() == CameraType.FRONT, canSwitchCamera = call?.canSwitchCamera() ?: false, diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 10c7cb2e24..157b97252d 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -88,6 +88,7 @@ class WebRtcCall(val mxCall: MxCall, private val dispatcher: CoroutineContext, private val sessionProvider: Provider, private val peerConnectionFactoryProvider: Provider, + private val onCallBecomeActive: (WebRtcCall) -> Unit, private val onCallEnded: (WebRtcCall) -> Unit) : MxCall.StateListener { interface Listener : MxCall.StateListener { @@ -130,8 +131,11 @@ class WebRtcCall(val mxCall: MxCall, // Mute status var micMuted = false + private set var videoMuted = false + private set var remoteOnHold = false + private set var offerSdp: CallInviteContent.Offer? = null @@ -328,6 +332,9 @@ class WebRtcCall(val mxCall: MxCall, } private suspend fun internalAcceptIncomingCall() = withContext(dispatcher) { + tryOrNull { + onCallBecomeActive(this@WebRtcCall) + } val turnServerResponse = getTurnServer() // Update service state withContext(Dispatchers.Main) { @@ -542,6 +549,9 @@ class WebRtcCall(val mxCall: MxCall, fun updateRemoteOnHold(onHold: Boolean) { if (remoteOnHold == onHold) return remoteOnHold = onHold + if (!onHold) { + onCallBecomeActive(this) + } val direction = if (onHold) { RtpTransceiver.RtpTransceiverDirection.INACTIVE } else { diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index f6fcfd446e..a484acbc5f 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -45,6 +45,7 @@ import org.webrtc.DefaultVideoDecoderFactory import org.webrtc.DefaultVideoEncoderFactory import org.webrtc.PeerConnectionFactory import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors import javax.inject.Inject import javax.inject.Singleton @@ -63,7 +64,7 @@ class WebRtcCallManager @Inject constructor( get() = activeSessionDataSource.currentValue?.orNull() interface CurrentCallListener { - fun onCurrentCallChange(call: WebRtcCall?) + fun onCurrentCallChange(call: WebRtcCall?) {} fun onAudioDevicesChange() {} } @@ -101,15 +102,15 @@ class WebRtcCallManager @Inject constructor( } var currentCall: WebRtcCall? = null - set(value) { + private set(value) { field = value currentCallsListeners.forEach { tryOrNull { it.onCurrentCallChange(value) } } } - private val callsByCallId = HashMap() - private val callsByRoomId = HashMap>() + private val callsByCallId = ConcurrentHashMap() + private val callsByRoomId = ConcurrentHashMap>() fun getCallById(callId: String): WebRtcCall? { return callsByCallId[callId] @@ -119,6 +120,10 @@ class WebRtcCallManager @Inject constructor( return callsByRoomId[roomId] ?: emptyList() } + fun getCalls(): List { + return callsByCallId.values.toList() + } + fun headSetButtonTapped() { Timber.v("## VOIP headSetButtonTapped") val call = currentCall ?: return @@ -161,13 +166,23 @@ class WebRtcCallManager @Inject constructor( .createPeerConnectionFactory() } + private fun onCallActive(call: WebRtcCall) { + Timber.v("## VOIP WebRtcPeerConnectionManager onCall active: ${call.mxCall.callId}") + if (currentCall != call) { + currentCall?.updateRemoteOnHold(onHold = true) + currentCall = call + } + } + private fun onCallEnded(call: WebRtcCall) { Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: ${call.mxCall.callId}") - CallService.onNoActiveCall(context) + CallService.onCallTerminated(context, call.callId) callAudioManager.stop() - currentCall = null callsByCallId.remove(call.mxCall.callId) callsByRoomId[call.mxCall.roomId]?.remove(call) + if (currentCall == call) { + currentCall = getCalls().lastOrNull() + } // This must be done in this thread executor.execute { if (currentCall == null) { @@ -181,12 +196,17 @@ class WebRtcCallManager @Inject constructor( fun startOutgoingCall(signalingRoomId: String, otherUserId: String, isVideoCall: Boolean) { Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") + if (currentCall != null && currentCall?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) { + Timber.w("## VOIP cannot start outgoing call") + // Just ignore, maybe we could answer from other session? + return + } executor.execute { createPeerConnectionFactoryIfNeeded() } - + currentCall?.updateRemoteOnHold(onHold = true) val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return - createWebRtcCall(mxCall) + currentCall = createWebRtcCall(mxCall) callAudioManager.startForCall(mxCall) CallService.onOutgoingCallRinging( @@ -199,10 +219,11 @@ class WebRtcCallManager @Inject constructor( override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) { Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId}") - if (currentCall?.mxCall?.callId != mxCall.callId) return Unit.also { - Timber.w("## VOIP ignore ice candidates from other call") - } - currentCall?.onCallIceCandidateReceived(iceCandidatesContent) + val call = callsByCallId[iceCandidatesContent.callId] + ?: return Unit.also { + Timber.w("onCallIceCandidateReceived for non active call? ${iceCandidatesContent.callId}") + } + call.onCallIceCandidateReceived(iceCandidatesContent) } private fun createWebRtcCall(mxCall: MxCall): WebRtcCall { @@ -217,21 +238,17 @@ class WebRtcCallManager @Inject constructor( peerConnectionFactory }, sessionProvider = { currentSession }, + onCallBecomeActive = this::onCallActive, onCallEnded = this::onCallEnded ) - currentCall = webRtcCall callsByCallId[mxCall.callId] = webRtcCall - callsByRoomId.getOrPut(mxCall.roomId) { ArrayList() } + callsByRoomId.getOrPut(mxCall.roomId) { ArrayList(1) } .add(webRtcCall) return webRtcCall } - fun acceptIncomingCall() { - currentCall?.acceptIncomingCall() - } - - fun endCall(originatedByMe: Boolean = true) { - currentCall?.endCall(originatedByMe) + fun endCallForRoom(roomId: String, originatedByMe: Boolean = true) { + callsByRoomId[roomId]?.forEach { it.endCall(originatedByMe) } } fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { @@ -248,8 +265,8 @@ class WebRtcCallManager @Inject constructor( override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) { Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}") - if (currentCall != null) { - Timber.w("## VOIP receiving incoming call while already in call?") + if (currentCall != null && currentCall?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) { + Timber.w("## VOIP receiving incoming call but cannot handle it") // Just ignore, maybe we could answer from other session? return } @@ -329,12 +346,12 @@ class WebRtcCallManager @Inject constructor( override fun onCallManagedByOtherSession(callId: String) { Timber.v("## VOIP onCallManagedByOtherSession: $callId") - currentCall = null val webRtcCall = callsByCallId.remove(callId) if (webRtcCall != null) { callsByRoomId[webRtcCall.mxCall.roomId]?.remove(webRtcCall) } - CallService.onNoActiveCall(context) + // TODO: handle this properly + CallService.onCallTerminated(context, callId) // did we start background sync? so we should stop it if (isInBackground) { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index d344f1bd86..8281f055e7 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -21,7 +21,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat -import androidx.lifecycle.Observer import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -37,7 +36,7 @@ import im.vector.app.core.ui.views.ActiveCallView import im.vector.app.core.ui.views.ActiveCallViewHolder import im.vector.app.core.ui.views.KeysBackupBanner import im.vector.app.databinding.FragmentHomeDetailBinding -import im.vector.app.features.call.SharedActiveCallViewModel +import im.vector.app.features.call.SharedCurrentCallViewModel import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.room.list.RoomListFragment @@ -78,7 +77,7 @@ class HomeDetailFragment @Inject constructor( private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel() private lateinit var sharedActionViewModel: HomeSharedActionViewModel - private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel + private lateinit var sharedCallActionViewModel: SharedCurrentCallViewModel override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeDetailBinding { return FragmentHomeDetailBinding.inflate(inflater, container, false) @@ -89,7 +88,7 @@ class HomeDetailFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java) - sharedCallActionViewModel = activityViewModelProvider.get(SharedActiveCallViewModel::class.java) + sharedCallActionViewModel = activityViewModelProvider.get(SharedCurrentCallViewModel::class.java) setupBottomNavigationView() setupToolbar() @@ -127,9 +126,9 @@ class HomeDetailFragment @Inject constructor( } sharedCallActionViewModel - .activeCall - .observe(viewLifecycleOwner, Observer { - activeCallViewHolder.updateCall(it) + .currentCall + .observe(viewLifecycleOwner, { + activeCallViewHolder.updateCall(it, callManager.getCalls()) invalidateOptionsMenu() }) } @@ -336,7 +335,7 @@ class HomeDetailFragment @Inject constructor( } override fun onTapToReturnToCall() { - sharedCallActionViewModel.activeCall.value?.let { call -> + sharedCallActionViewModel.currentCall.value?.let { call -> VectorCallActivity.newIntent( context = requireContext(), callId = call.callId, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 988cf2fd30..62e00f03be 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -120,7 +120,7 @@ import im.vector.app.features.attachments.ContactAttachment import im.vector.app.features.attachments.preview.AttachmentsPreviewActivity import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs import im.vector.app.features.attachments.toGroupedContentAttachmentData -import im.vector.app.features.call.SharedActiveCallViewModel +import im.vector.app.features.call.SharedCurrentCallViewModel import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.webrtc.WebRtcCallManager @@ -227,7 +227,8 @@ class RoomDetailFragment @Inject constructor( private val matrixItemColorProvider: MatrixItemColorProvider, private val imageContentRenderer: ImageContentRenderer, private val roomDetailPendingActionStore: RoomDetailPendingActionStore, - private val pillsPostProcessorFactory: PillsPostProcessor.Factory + private val pillsPostProcessorFactory: PillsPostProcessor.Factory, + private val callManager: WebRtcCallManager ) : VectorBaseFragment(), TimelineEventController.Callback, @@ -282,7 +283,7 @@ class RoomDetailFragment @Inject constructor( override fun getMenuRes() = R.menu.menu_timeline private lateinit var sharedActionViewModel: MessageSharedActionViewModel - private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel + private lateinit var sharedCurrentCallViewModel: SharedCurrentCallViewModel private lateinit var layoutManager: LinearLayoutManager private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager @@ -299,7 +300,7 @@ class RoomDetailFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) - sharedCallActionViewModel = activityViewModelProvider.get(SharedActiveCallViewModel::class.java) + sharedCurrentCallViewModel = activityViewModelProvider.get(SharedCurrentCallViewModel::class.java) attachmentsHelper = AttachmentsHelper(requireContext(), this).register() keyboardStateUtils = KeyboardStateUtils(requireActivity()) setupToolbar(views.roomToolbar) @@ -324,10 +325,10 @@ class RoomDetailFragment @Inject constructor( } .disposeOnDestroyView() - sharedCallActionViewModel - .activeCall + sharedCurrentCallViewModel + .currentCall .observe(viewLifecycleOwner, { - activeCallViewHolder.updateCall(it) + activeCallViewHolder.updateCall(it, callManager.getCalls()) invalidateOptionsMenu() }) @@ -799,8 +800,8 @@ class RoomDetailFragment @Inject constructor( showDialogWithMessage(getString(R.string.cannot_call_yourself)) } } - 2 -> { - val activeCall = sharedCallActionViewModel.activeCall.value + 2 -> { + val activeCall = sharedCurrentCallViewModel.currentCall.value if (activeCall != null) { // resume existing if same room, if not prompt to kill and then restart new call? if (activeCall.roomId == roomDetailArgs.roomId) { @@ -2015,7 +2016,7 @@ class RoomDetailFragment @Inject constructor( } override fun onTapToReturnToCall() { - sharedCallActionViewModel.activeCall.value?.let { call -> + sharedCurrentCallViewModel.currentCall.value?.let { call -> VectorCallActivity.newIntent( context = requireContext(), callId = call.callId, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index a8cd200814..ab4236489b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -349,7 +349,7 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleEndCall() { - callManager.endCall() + callManager.endCallForRoom(initialState.roomId) } private fun handleSelectStickerAttachment() { diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 74a93fceda..2ec917c739 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -450,6 +450,7 @@ class NotificationUtils @Inject constructor(private val context: Context, fun buildCallEndedNotification(): Notification { return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) .setContentTitle(stringProvider.getString(R.string.call_ended)) + .setTimeoutAfter(2000) .setSmallIcon(R.drawable.ic_material_call_end_grey) .setCategory(NotificationCompat.CATEGORY_CALL) .build() diff --git a/vector/src/main/res/layout/view_active_call_view.xml b/vector/src/main/res/layout/view_active_call_view.xml index e8b21de7e8..6fb585865e 100644 --- a/vector/src/main/res/layout/view_active_call_view.xml +++ b/vector/src/main/res/layout/view_active_call_view.xml @@ -20,10 +20,11 @@ android:paddingTop="12dp" android:paddingEnd="16dp" android:paddingBottom="12dp" - android:text="@string/active_call" + android:textSize="14sp" + android:text="@string/call_one_active_one_paused_call" android:textColor="@color/white" app:drawableTint="@color/white" - app:drawableStartCompat="@drawable/ic_call" /> + app:drawableStartCompat="@drawable/ic_call_answer" /> diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 61f79444c7..508c8a2c31 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -141,6 +141,7 @@ Unpublish Copied to clipboard Disable + Return Confirmation @@ -2773,4 +2774,9 @@ %1$s declined this call This call has ended Call back + + Active call (%1$s) + 1 active call (%1$s) · 1 paused call + 2 paused calls + From b1f492de5871bc990944aa171ac5a8f5ebf5ea82 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 11 Dec 2020 19:18:48 +0100 Subject: [PATCH 22/57] QueueMemento : fix synchronized --- .../session/room/send/queue/QueueMemento.kt | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt index dfbac347d9..a6836c8086 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt @@ -49,8 +49,10 @@ internal class QueueMemento @Inject constructor(context: Context, } fun unTrack(task: QueuedTask) { - managedTaskInfos.remove(task) - persist() + synchronized(managedTaskInfos) { + managedTaskInfos.remove(task) + persist() + } } private fun persist() { @@ -64,19 +66,17 @@ internal class QueueMemento @Inject constructor(context: Context, } private fun toTaskInfo(task: QueuedTask, order: Int): TaskInfo? { - synchronized(managedTaskInfos) { - return when (task) { - is SendEventQueuedTask -> SendEventTaskInfo( - localEchoId = task.event.eventId ?: "", - encrypt = task.encrypt, - order = order - ) - is RedactQueuedTask -> RedactEventTaskInfo( - redactionLocalEcho = task.redactionLocalEchoId, - order = order - ) - else -> null - } + return when (task) { + is SendEventQueuedTask -> SendEventTaskInfo( + localEchoId = task.event.eventId ?: "", + encrypt = task.encrypt, + order = order + ) + is RedactQueuedTask -> RedactEventTaskInfo( + redactionLocalEcho = task.redactionLocalEchoId, + order = order + ) + else -> null } } @@ -90,7 +90,7 @@ internal class QueueMemento @Inject constructor(context: Context, ?.forEach { info -> try { when (info) { - is SendEventTaskInfo -> { + is SendEventTaskInfo -> { localEchoRepository.getUpToDateEcho(info.localEchoId)?.let { if (it.sendState.isSending() && it.eventId != null && it.roomId != null) { localEchoRepository.updateSendState(it.eventId, it.roomId, SendState.UNSENT) From a5736efc7504ff83bca9ee4e742f6af074610a2b Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 22 Dec 2020 12:05:36 +0100 Subject: [PATCH 23/57] VoIP: add info on other call when switching --- .../im/vector/app/core/di/ViewModelModule.kt | 6 +-- .../app/core/ui/views/ActiveCallViewHolder.kt | 4 +- ...{ActiveCallView.kt => CurrentCallsView.kt} | 33 +++++++----- ...wModel.kt => SharedKnownCallsViewModel.kt} | 36 ++++++++----- .../app/features/call/VectorCallActivity.kt | 26 ++++++++-- .../app/features/call/VectorCallViewModel.kt | 23 ++++++++- .../app/features/call/VectorCallViewState.kt | 10 +++- .../app/features/call/webrtc/WebRtcCall.kt | 28 +++++----- .../features/call/webrtc/WebRtcCallManager.kt | 51 +++++++++++-------- .../app/features/home/HomeDetailFragment.kt | 16 +++--- .../home/room/detail/RoomDetailFragment.kt | 31 +++++------ .../app/features/navigation/Navigator.kt | 1 + .../app/features/popup/IncomingCallAlert.kt | 3 +- .../features/popup/VerificationVectorAlert.kt | 3 +- vector/src/main/res/layout/activity_call.xml | 43 +++++++++++++--- .../main/res/layout/fragment_home_detail.xml | 2 +- .../main/res/layout/fragment_room_detail.xml | 2 +- ...e_call_view.xml => view_current_calls.xml} | 9 ++-- vector/src/main/res/values/strings.xml | 10 ++-- 19 files changed, 220 insertions(+), 117 deletions(-) rename vector/src/main/java/im/vector/app/core/ui/views/{ActiveCallView.kt => CurrentCallsView.kt} (59%) rename vector/src/main/java/im/vector/app/features/call/{SharedCurrentCallViewModel.kt => SharedKnownCallsViewModel.kt} (56%) rename vector/src/main/res/layout/{view_active_call_view.xml => view_current_calls.xml} (85%) diff --git a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt index 9d5c0d5491..8409021845 100644 --- a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt @@ -22,7 +22,7 @@ import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import im.vector.app.core.platform.ConfigurationViewModel -import im.vector.app.features.call.SharedCurrentCallViewModel +import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel @@ -85,8 +85,8 @@ interface ViewModelModule { @Binds @IntoMap - @ViewModelKey(SharedCurrentCallViewModel::class) - fun bindSharedActiveCallViewModel(viewModel: SharedCurrentCallViewModel): ViewModel + @ViewModelKey(SharedKnownCallsViewModel::class) + fun bindSharedActiveCallViewModel(viewModel: SharedKnownCallsViewModel): ViewModel @Binds @IntoMap diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt b/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt index fdf3b99986..adee15afe3 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt @@ -28,7 +28,7 @@ import org.webrtc.SurfaceViewRenderer class ActiveCallViewHolder { private var activeCallPiP: SurfaceViewRenderer? = null - private var activeCallView: ActiveCallView? = null + private var activeCallView: CurrentCallsView? = null private var pipWrapper: CardView? = null private var activeCall: WebRtcCall? = null @@ -70,7 +70,7 @@ class ActiveCallViewHolder { } } - fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: ActiveCallView, pipWrapper: CardView, interactionListener: ActiveCallView.Callback) { + fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: CurrentCallsView, pipWrapper: CardView, interactionListener: CurrentCallsView.Callback) { this.activeCallPiP = activeCallPiP this.activeCallView = activeCallView this.pipWrapper = pipWrapper diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt similarity index 59% rename from vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt rename to vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt index fd3159c2e0..8fd7bb4c90 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt @@ -22,10 +22,10 @@ import android.widget.RelativeLayout import im.vector.app.R import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.themes.ThemeUtils -import kotlinx.android.synthetic.main.view_active_call_view.view.* +import kotlinx.android.synthetic.main.view_current_calls.view.* import org.matrix.android.sdk.api.session.call.CallState -class ActiveCallView @JvmOverloads constructor( +class CurrentCallsView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 @@ -42,25 +42,32 @@ class ActiveCallView @JvmOverloads constructor( } private fun setupView() { - inflate(context, R.layout.view_active_call_view, this) + inflate(context, R.layout.view_current_calls, this) setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary)) setOnClickListener { callback?.onTapToReturnToCall() } } fun render(calls: List) { - if (calls.size == 1) { - activeCallInfo.setText(R.string.call_active_call) - } else if (calls.size == 2) { - val activeCall = calls.firstOrNull { - it.mxCall.state is CallState.Connected && !it.isLocalOnHold() - } - if (activeCall == null) { - activeCallInfo.setText(R.string.call_two_paused_calls) + val connectedCalls = calls.filter { + it.mxCall.state is CallState.Connected + } + val heldCalls = connectedCalls.filter { + it.isLocalOnHold() || it.remoteOnHold + } + if (connectedCalls.size == 1) { + if (heldCalls.size == 1) { + currentCallsInfo.setText(R.string.call_only_paused) } else { - activeCallInfo.setText(R.string.call_one_active_one_paused_call) + currentCallsInfo.setText(R.string.call_only_active) } } else { - visibility = GONE + if (heldCalls.size > 1) { + currentCallsInfo.text = resources.getString(R.string.call_only_multiple_paused , heldCalls.size) + } else if (heldCalls.size == 1) { + currentCallsInfo.setText(R.string.call_active_and_single_paused) + } else { + currentCallsInfo.text = resources.getString(R.string.call_active_and_multiple_paused, "00:00", heldCalls.size) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/call/SharedCurrentCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt similarity index 56% rename from vector/src/main/java/im/vector/app/features/call/SharedCurrentCallViewModel.kt rename to vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt index d823fa6d5b..685b23f332 100644 --- a/vector/src/main/java/im/vector/app/features/call/SharedCurrentCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt @@ -23,43 +23,51 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import org.matrix.android.sdk.api.session.call.MxCall import javax.inject.Inject -class SharedCurrentCallViewModel @Inject constructor( +class SharedKnownCallsViewModel @Inject constructor( private val callManager: WebRtcCallManager ) : ViewModel() { - val currentCall: MutableLiveData = MutableLiveData() + val liveKnownCalls: MutableLiveData> = MutableLiveData() - val callStateListener = object : WebRtcCall.Listener { + val callListener = object : WebRtcCall.Listener { override fun onStateUpdate(call: MxCall) { //post it-self - currentCall.postValue(currentCall.value) + liveKnownCalls.postValue(liveKnownCalls.value) } override fun onHoldUnhold() { super.onHoldUnhold() //post it-self - currentCall.postValue(currentCall.value) + liveKnownCalls.postValue(liveKnownCalls.value) } - } - private val listener = object : WebRtcCallManager.CurrentCallListener { + private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { override fun onCurrentCallChange(call: WebRtcCall?) { - currentCall.value?.mxCall?.removeListener(callStateListener) - currentCall.postValue(call) - call?.addListener(callStateListener) + val knownCalls = callManager.getCalls() + liveKnownCalls.postValue(knownCalls) + knownCalls.forEach { + it.removeListener(callListener) + it.addListener(callListener) + } } } init { - currentCall.postValue(callManager.currentCall) - callManager.addCurrentCallListener(listener) + val knownCalls = callManager.getCalls() + liveKnownCalls.postValue(knownCalls) + callManager.addCurrentCallListener(currentCallListener) + knownCalls.forEach { + it.addListener(callListener) + } } override fun onCleared() { - currentCall.value?.removeListener(callStateListener) - callManager.removeCurrentCallListener(listener) + callManager.getCalls().forEach { + it.removeListener(callListener) + } + callManager.removeCurrentCallListener(currentCallListener) super.onCleared() } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 2e5b54ae9a..5f7bf1802a 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -34,10 +34,10 @@ import androidx.core.view.isVisible import com.airbnb.mvrx.Fail import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.viewModel +import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.di.ScreenComponent import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.core.services.CallService import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.app.core.utils.allGranted @@ -50,6 +50,7 @@ import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCallDetail import org.matrix.android.sdk.api.session.call.MxPeerConnectionState @@ -199,7 +200,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callStatusText.setText(R.string.call_held_by_you) } else { views.callActionText.isInvisible = true - state.otherUserMatrixItem.invoke()?.let { + state.callInfo.otherUserItem?.let { views.callStatusText.text = getString(R.string.call_held_by_user, it.getBestName()) } } @@ -208,7 +209,8 @@ class VectorCallActivity : VectorBaseActivity(), CallContro if (callArgs.isVideoCall) { views.callVideoGroup.isVisible = true views.callInfoGroup.isVisible = false - //views.pip_video_view.isVisible = !state.isVideoCaptureInError + views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null + configureCallInfo(state) } else { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true @@ -235,7 +237,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } private fun configureCallInfo(state: VectorCallViewState, blurAvatar: Boolean = false) { - state.otherUserMatrixItem.invoke()?.let { + state.callInfo.otherUserItem?.let { val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter) views.participantNameText.text = it.getBestName() @@ -245,10 +247,26 @@ class VectorCallActivity : VectorBaseActivity(), CallContro avatarRenderer.render(it, views.otherMemberAvatar) } } + if (state.otherKnownCallInfo?.otherUserItem == null) { + views.otherKnownCallLayout.isVisible = false + } else { + val otherCall = callManager.getCallById(state.otherKnownCallInfo.callId) + val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) + avatarRenderer.renderBlur(state.otherKnownCallInfo.otherUserItem, views.otherKnownCallAvatarView, sampling = 20, rounded = false, colorFilter = colorFilter) + views.otherKnownCallLayout.isVisible = true + views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold() || it.remoteOnHold }.orFalse() + } } private fun configureCallViews() { views.callControlsView.interactionListener = this + views.otherKnownCallAvatarView.setOnClickListener { + withState(callViewModel) { + val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState + startActivity(newIntent(this, otherCall.mxCall, null)) + finish() + } + } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index ecea4f1cb1..7e3977bc99 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -20,7 +20,6 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -117,6 +116,10 @@ class VectorCallViewModel @AssistedInject constructor( private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { + override fun onCurrentCallChange(call: WebRtcCall?) { + updateOtherKnownCall(call) + } + override fun onAudioDevicesChange() { val currentSoundDevice = callManager.callAudioManager.getCurrentSoundDevice() if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { @@ -134,6 +137,21 @@ class VectorCallViewModel @AssistedInject constructor( } } + private fun updateOtherKnownCall(currentCall: WebRtcCall?) { + if (currentCall == null) return + val otherCall = callManager.getCalls().firstOrNull { + it.callId != currentCall.callId && it.mxCall.state is CallState.Connected + } + setState { + if (otherCall == null) { + copy(otherKnownCallInfo = null) + } else { + val otherUserItem: MatrixItem? = session.getUser(otherCall.mxCall.opponentUserId)?.toMatrixItem() + copy(otherKnownCallInfo = VectorCallViewState.CallInfo(otherCall.callId, otherUserItem)) + } + } + } + init { val webRtcCall = callManager.getCallById(initialState.callId) if (webRtcCall == null) { @@ -153,7 +171,7 @@ class VectorCallViewModel @AssistedInject constructor( copy( isVideoCall = webRtcCall.mxCall.isVideoCall, callState = Success(webRtcCall.mxCall.state), - otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized, + callInfo = VectorCallViewState.CallInfo(callId, item), soundDevice = currentSoundDevice, isLocalOnHold = webRtcCall.isLocalOnHold(), isRemoteOnHold = webRtcCall.remoteOnHold, @@ -163,6 +181,7 @@ class VectorCallViewModel @AssistedInject constructor( isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD ) } + updateOtherKnownCall(webRtcCall) } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt index 8742e2cc7c..aba109091a 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt @@ -36,10 +36,16 @@ data class VectorCallViewState( val canSwitchCamera: Boolean = true, val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE, val availableSoundDevices: List = emptyList(), - val otherUserMatrixItem: Async = Uninitialized, - val callState: Async = Uninitialized + val callState: Async = Uninitialized, + val otherKnownCallInfo: CallInfo? = null, + val callInfo: CallInfo = CallInfo(callId) ) : MvRxState { + data class CallInfo( + val callId: String, + val otherUserItem: MatrixItem? = null + ) + constructor(callArgs: CallArgs): this( callId = callArgs.callId, roomId = callArgs.roomId, diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 157b97252d..1b7775a5d7 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -72,6 +72,7 @@ import org.webrtc.VideoSource import org.webrtc.VideoTrack import timber.log.Timber import java.lang.ref.WeakReference +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit import javax.inject.Provider import kotlin.coroutines.CoroutineContext @@ -97,7 +98,7 @@ class WebRtcCall(val mxCall: MxCall, fun onHoldUnhold() {} } - private val listeners = ArrayList() + private val listeners = CopyOnWriteArrayList() fun addListener(listener: Listener) { listeners.add(listener) @@ -277,7 +278,7 @@ class WebRtcCall(val mxCall: MxCall, } } - fun detachRenderers(renderers: List?) { + fun detachRenderers(renderers: List?) = synchronized(this) { Timber.v("## VOIP detachRenderers") // currentCall?.localMediaStream?.let { currentCall?.peerConnection?.removeStream(it) } if (renderers.isNullOrEmpty()) { @@ -533,7 +534,7 @@ class WebRtcCall(val mxCall: MxCall, * rather than 'sendonly') * @returns true if the other party has put us on hold */ - fun isLocalOnHold(): Boolean { + fun isLocalOnHold(): Boolean = synchronized(this) { if (mxCall.state !is CallState.Connected) return false var callOnHold = true // We consider a call to be on hold only if *all* the tracks are on hold @@ -546,7 +547,7 @@ class WebRtcCall(val mxCall: MxCall, return callOnHold } - fun updateRemoteOnHold(onHold: Boolean) { + fun updateRemoteOnHold(onHold: Boolean) = synchronized(this){ if (remoteOnHold == onHold) return remoteOnHold = onHold if (!onHold) { @@ -563,21 +564,21 @@ class WebRtcCall(val mxCall: MxCall, updateMuteStatus() } - fun muteCall(muted: Boolean) { + fun muteCall(muted: Boolean) = synchronized(this) { micMuted = muted updateMuteStatus() } - fun enableVideo(enabled: Boolean) { + fun enableVideo(enabled: Boolean) = synchronized(this) { videoMuted = !enabled updateMuteStatus() } - fun canSwitchCamera(): Boolean { + fun canSwitchCamera(): Boolean = synchronized(this){ return availableCamera.size > 1 } - private fun getOppositeCameraIfAny(): CameraProxy? { + private fun getOppositeCameraIfAny(): CameraProxy? = synchronized(this){ val currentCamera = cameraInUse ?: return null return if (currentCamera.type == CameraType.FRONT) { availableCamera.firstOrNull { it.type == CameraType.BACK } @@ -586,7 +587,7 @@ class WebRtcCall(val mxCall: MxCall, } } - fun switchCamera() { + fun switchCamera() = synchronized(this){ Timber.v("## VOIP switchCamera") if (mxCall.state is CallState.Connected && mxCall.isVideoCall) { val oppositeCamera = getOppositeCameraIfAny() ?: return @@ -629,15 +630,15 @@ class WebRtcCall(val mxCall: MxCall, } } - fun currentCameraType(): CameraType? { + fun currentCameraType(): CameraType? = synchronized(this){ return cameraInUse?.type } - fun currentCaptureFormat(): CaptureFormat { + fun currentCaptureFormat(): CaptureFormat = synchronized(this) { return currentCaptureFormat } - private fun release() { + private fun release() { mxCall.removeListener(this) videoCapturer?.stopCapture() videoCapturer?.dispose() @@ -689,7 +690,7 @@ class WebRtcCall(val mxCall: MxCall, } } - fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) { + fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) = synchronized(this) { if (mxCall.state == CallState.Terminated) { return } @@ -702,6 +703,7 @@ class WebRtcCall(val mxCall: MxCall, cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) } release() + listeners.clear() onCallEnded(this) if (originatedByMe) { // send hang up event diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index a484acbc5f..883b966701 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -46,7 +46,9 @@ import org.webrtc.DefaultVideoEncoderFactory import org.webrtc.PeerConnectionFactory import timber.log.Timber import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject import javax.inject.Singleton @@ -68,7 +70,7 @@ class WebRtcCallManager @Inject constructor( fun onAudioDevicesChange() {} } - private val currentCallsListeners = emptyList().toMutableList() + private val currentCallsListeners = CopyOnWriteArrayList() fun addCurrentCallListener(listener: CurrentCallListener) { currentCallsListeners.add(listener) } @@ -101,13 +103,17 @@ class WebRtcCallManager @Inject constructor( isInBackground = true } - var currentCall: WebRtcCall? = null - private set(value) { - field = value - currentCallsListeners.forEach { - tryOrNull { it.onCurrentCallChange(value) } - } + /** + * The current call is the call we interacted with whatever his state (connected,resumed, held...) + * As soon as we interact with an other call, it replaces this one and put it on held if not already. + */ + var currentCall: AtomicReference = AtomicReference(null) + private fun AtomicReference.setAndNotify(newValue: WebRtcCall?) { + set(newValue) + currentCallsListeners.forEach { + tryOrNull { it.onCurrentCallChange(newValue) } } + } private val callsByCallId = ConcurrentHashMap() private val callsByRoomId = ConcurrentHashMap>() @@ -120,13 +126,17 @@ class WebRtcCallManager @Inject constructor( return callsByRoomId[roomId] ?: emptyList() } + fun getCurrentCall(): WebRtcCall? { + return currentCall.get() + } + fun getCalls(): List { return callsByCallId.values.toList() } fun headSetButtonTapped() { Timber.v("## VOIP headSetButtonTapped") - val call = currentCall ?: return + val call = currentCall.get() ?: return if (call.mxCall.state is CallState.LocalRinging) { // accept call call.acceptIncomingCall() @@ -168,10 +178,9 @@ class WebRtcCallManager @Inject constructor( private fun onCallActive(call: WebRtcCall) { Timber.v("## VOIP WebRtcPeerConnectionManager onCall active: ${call.mxCall.callId}") - if (currentCall != call) { - currentCall?.updateRemoteOnHold(onHold = true) - currentCall = call - } + val currentCall = currentCall.get().takeIf { it != call } + currentCall?.updateRemoteOnHold(onHold = true) + this.currentCall.setAndNotify(call) } private fun onCallEnded(call: WebRtcCall) { @@ -180,12 +189,13 @@ class WebRtcCallManager @Inject constructor( callAudioManager.stop() callsByCallId.remove(call.mxCall.callId) callsByRoomId[call.mxCall.roomId]?.remove(call) - if (currentCall == call) { - currentCall = getCalls().lastOrNull() + if (currentCall.get() == call) { + val otherCall = getCalls().lastOrNull() + currentCall.setAndNotify(otherCall) } // This must be done in this thread executor.execute { - if (currentCall == null) { + if (currentCall.get() == null) { Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one") peerConnectionFactory?.dispose() peerConnectionFactory = null @@ -196,7 +206,7 @@ class WebRtcCallManager @Inject constructor( fun startOutgoingCall(signalingRoomId: String, otherUserId: String, isVideoCall: Boolean) { Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") - if (currentCall != null && currentCall?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) { + if (currentCall.get() != null && currentCall.get()?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) { Timber.w("## VOIP cannot start outgoing call") // Just ignore, maybe we could answer from other session? return @@ -204,9 +214,10 @@ class WebRtcCallManager @Inject constructor( executor.execute { createPeerConnectionFactoryIfNeeded() } - currentCall?.updateRemoteOnHold(onHold = true) + currentCall.get()?.updateRemoteOnHold(onHold = true) val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return - currentCall = createWebRtcCall(mxCall) + val webRtcCall = createWebRtcCall(mxCall) + currentCall.setAndNotify(webRtcCall) callAudioManager.startForCall(mxCall) CallService.onOutgoingCallRinging( @@ -253,7 +264,7 @@ class WebRtcCallManager @Inject constructor( fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { Timber.v("## VOIP onWiredDeviceEvent $event") - currentCall ?: return + currentCall.get() ?: return // sometimes we received un-wanted unplugged... callAudioManager.wiredStateChange(event) } @@ -265,7 +276,7 @@ class WebRtcCallManager @Inject constructor( override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) { Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}") - if (currentCall != null && currentCall?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) { + if (currentCall.get() != null && currentCall.get()?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) { Timber.w("## VOIP receiving incoming call but cannot handle it") // Just ignore, maybe we could answer from other session? return diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 8281f055e7..87b561ff93 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -32,11 +32,11 @@ import im.vector.app.core.glide.GlideApp import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.core.ui.views.ActiveCallView +import im.vector.app.core.ui.views.CurrentCallsView import im.vector.app.core.ui.views.ActiveCallViewHolder import im.vector.app.core.ui.views.KeysBackupBanner import im.vector.app.databinding.FragmentHomeDetailBinding -import im.vector.app.features.call.SharedCurrentCallViewModel +import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.room.list.RoomListFragment @@ -69,7 +69,7 @@ class HomeDetailFragment @Inject constructor( private val vectorPreferences: VectorPreferences ) : VectorBaseFragment(), KeysBackupBanner.Delegate, - ActiveCallView.Callback, + CurrentCallsView.Callback, ServerBackupStatusViewModel.Factory { private val viewModel: HomeDetailViewModel by fragmentViewModel() @@ -77,7 +77,7 @@ class HomeDetailFragment @Inject constructor( private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel() private lateinit var sharedActionViewModel: HomeSharedActionViewModel - private lateinit var sharedCallActionViewModel: SharedCurrentCallViewModel + private lateinit var sharedCallActionViewModel: SharedKnownCallsViewModel override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeDetailBinding { return FragmentHomeDetailBinding.inflate(inflater, container, false) @@ -88,7 +88,7 @@ class HomeDetailFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java) - sharedCallActionViewModel = activityViewModelProvider.get(SharedCurrentCallViewModel::class.java) + sharedCallActionViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) setupBottomNavigationView() setupToolbar() @@ -126,9 +126,9 @@ class HomeDetailFragment @Inject constructor( } sharedCallActionViewModel - .currentCall + .liveKnownCalls .observe(viewLifecycleOwner, { - activeCallViewHolder.updateCall(it, callManager.getCalls()) + activeCallViewHolder.updateCall(callManager.getCurrentCall(), callManager.getCalls()) invalidateOptionsMenu() }) } @@ -335,7 +335,7 @@ class HomeDetailFragment @Inject constructor( } override fun onTapToReturnToCall() { - sharedCallActionViewModel.currentCall.value?.let { call -> + callManager.getCurrentCall()?.let { call -> VectorCallActivity.newIntent( context = requireContext(), callId = call.callId, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 62e00f03be..958e956fee 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -89,7 +89,7 @@ import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.ui.views.ActiveCallView +import im.vector.app.core.ui.views.CurrentCallsView import im.vector.app.core.ui.views.ActiveCallViewHolder import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.JumpToReadMarkerView @@ -120,7 +120,7 @@ import im.vector.app.features.attachments.ContactAttachment import im.vector.app.features.attachments.preview.AttachmentsPreviewActivity import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs import im.vector.app.features.attachments.toGroupedContentAttachmentData -import im.vector.app.features.call.SharedCurrentCallViewModel +import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.webrtc.WebRtcCallManager @@ -237,7 +237,7 @@ class RoomDetailFragment @Inject constructor( AttachmentTypeSelectorView.Callback, AttachmentsHelper.Callback, GalleryOrCameraDialogHelper.Listener, - ActiveCallView.Callback { + CurrentCallsView.Callback { companion object { /** @@ -283,7 +283,7 @@ class RoomDetailFragment @Inject constructor( override fun getMenuRes() = R.menu.menu_timeline private lateinit var sharedActionViewModel: MessageSharedActionViewModel - private lateinit var sharedCurrentCallViewModel: SharedCurrentCallViewModel + private lateinit var knownCallsViewModel: SharedKnownCallsViewModel private lateinit var layoutManager: LinearLayoutManager private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager @@ -300,7 +300,7 @@ class RoomDetailFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) - sharedCurrentCallViewModel = activityViewModelProvider.get(SharedCurrentCallViewModel::class.java) + knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) attachmentsHelper = AttachmentsHelper(requireContext(), this).register() keyboardStateUtils = KeyboardStateUtils(requireActivity()) setupToolbar(views.roomToolbar) @@ -325,10 +325,10 @@ class RoomDetailFragment @Inject constructor( } .disposeOnDestroyView() - sharedCurrentCallViewModel - .currentCall + knownCallsViewModel + .liveKnownCalls .observe(viewLifecycleOwner, { - activeCallViewHolder.updateCall(it, callManager.getCalls()) + activeCallViewHolder.updateCall(callManager.getCurrentCall(), it) invalidateOptionsMenu() }) @@ -801,17 +801,14 @@ class RoomDetailFragment @Inject constructor( } } 2 -> { - val activeCall = sharedCurrentCallViewModel.currentCall.value - if (activeCall != null) { + val currentCall = callManager.getCurrentCall() + if (currentCall != null) { // resume existing if same room, if not prompt to kill and then restart new call? - if (activeCall.roomId == roomDetailArgs.roomId) { + if (currentCall.mxCall.roomId == roomDetailArgs.roomId) { onTapToReturnToCall() + }else { + safeStartCall(isVideoCall) } - // else { - // TODO might not work well, and should prompt - // webRtcPeerConnectionManager.endCall() - // safeStartCall(it, isVideoCall) - // } } else if (!state.isAllowedToStartWebRTCCall) { showDialogWithMessage(getString( if (state.isDm()) { @@ -2016,7 +2013,7 @@ class RoomDetailFragment @Inject constructor( } override fun onTapToReturnToCall() { - sharedCurrentCallViewModel.currentCall.value?.let { call -> + callManager.getCurrentCall()?.let { call -> VectorCallActivity.newIntent( context = requireContext(), callId = call.callId, diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 504fccb63a..21a8d42848 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -113,4 +113,5 @@ interface Navigator { options: ((MutableList>) -> Unit)?) fun openSearch(context: Context, roomId: String) + } diff --git a/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt b/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt index 64e729e54d..b3882945b4 100644 --- a/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt +++ b/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt @@ -21,6 +21,7 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import im.vector.app.R +import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem @@ -50,7 +51,7 @@ class IncomingCallAlert(uid: String, view.findViewById(R.id.incomingCallKindView).setText(callKind) view.findViewById(R.id.incomingCallNameView).text = matrixItem?.getBestName() view.findViewById(R.id.incomingCallAvatar)?.let { imageView -> - matrixItem?.let { avatarRenderer.render(it, imageView) } + matrixItem?.let { avatarRenderer.render(it, imageView, GlideApp.with(view.context.applicationContext)) } } view.findViewById(R.id.incomingCallAcceptView).setOnClickListener { onAccept() diff --git a/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt b/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt index 2cd9ab59ac..ee6728f969 100644 --- a/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt +++ b/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt @@ -21,6 +21,7 @@ import android.view.View import android.widget.ImageView import androidx.annotation.DrawableRes import im.vector.app.R +import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem @@ -43,7 +44,7 @@ class VerificationVectorAlert(uid: String, override fun bind(view: View) { view.findViewById(R.id.ivUserAvatar)?.let { imageView -> - matrixItem?.let { avatarRenderer.render(it, imageView) } + matrixItem?.let { avatarRenderer.render(it, imageView, GlideApp.with(view.context.applicationContext)) } } } } diff --git a/vector/src/main/res/layout/activity_call.xml b/vector/src/main/res/layout/activity_call.xml index b82915d383..45be67ae8e 100644 --- a/vector/src/main/res/layout/activity_call.xml +++ b/vector/src/main/res/layout/activity_call.xml @@ -14,10 +14,10 @@ + android:layout_height="match_parent" + android:scaleType="centerCrop" + tools:src="@tools:sample/avatars" /> + + + + + + + + + app:layout_constraintTop_toTopOf="@id/otherMemberAvatar" /> - - @@ -31,8 +30,8 @@ style="@style/Widget.MaterialComponents.Button.TextButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignTop="@+id/activeCallInfo" - android:layout_alignBottom="@+id/activeCallInfo" + android:layout_alignTop="@+id/currentCallsInfo" + android:layout_alignBottom="@+id/currentCallsInfo" android:layout_alignParentEnd="true" android:clickable="false" android:focusable="false" diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 508c8a2c31..190eae97bc 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2775,8 +2775,12 @@ This call has ended Call back - Active call (%1$s) - 1 active call (%1$s) · 1 paused call - 2 paused calls + + Active call (%1$s) + Paused call + 1 active call (%1$s) · 1 paused call + 1 active call (%1$s) · %2$d paused calls + %1$d paused calls + From 81f7932cb79116ca432e17d97d6b5c4159d3d884 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 22 Dec 2020 12:26:10 +0100 Subject: [PATCH 24/57] VoIP: show duration --- .../app/core/ui/views/CurrentCallsView.kt | 22 ++--- ...lViewHolder.kt => KnownCallsViewHolder.kt} | 46 +++++---- .../im/vector/app/core/utils/CountUpTimer.kt | 97 +++++++++++++++++++ .../app/features/call/VectorCallActivity.kt | 3 +- .../app/features/call/VectorCallViewModel.kt | 11 ++- .../app/features/call/VectorCallViewState.kt | 3 +- .../app/features/call/webrtc/WebRtcCall.kt | 59 +++++++++-- .../app/features/home/HomeDetailFragment.kt | 4 +- .../home/room/detail/RoomDetailFragment.kt | 4 +- 9 files changed, 207 insertions(+), 42 deletions(-) rename vector/src/main/java/im/vector/app/core/ui/views/{ActiveCallViewHolder.kt => KnownCallsViewHolder.kt} (66%) create mode 100644 vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt diff --git a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt index 8fd7bb4c90..16cb7785a7 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt @@ -18,11 +18,13 @@ package im.vector.app.core.ui.views import android.content.Context import android.util.AttributeSet +import android.view.LayoutInflater import android.widget.RelativeLayout import im.vector.app.R +import im.vector.app.databinding.ViewCallControlsBinding +import im.vector.app.databinding.ViewCurrentCallsBinding import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.themes.ThemeUtils -import kotlinx.android.synthetic.main.view_current_calls.view.* import org.matrix.android.sdk.api.session.call.CallState class CurrentCallsView @JvmOverloads constructor( @@ -35,19 +37,17 @@ class CurrentCallsView @JvmOverloads constructor( fun onTapToReturnToCall() } + val views: ViewCurrentCallsBinding var callback: Callback? = null init { - setupView() - } - - private fun setupView() { inflate(context, R.layout.view_current_calls, this) + views = ViewCurrentCallsBinding.bind(this) setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary)) setOnClickListener { callback?.onTapToReturnToCall() } } - fun render(calls: List) { + fun render(calls: List, formattedDuration: String) { val connectedCalls = calls.filter { it.mxCall.state is CallState.Connected } @@ -56,17 +56,17 @@ class CurrentCallsView @JvmOverloads constructor( } if (connectedCalls.size == 1) { if (heldCalls.size == 1) { - currentCallsInfo.setText(R.string.call_only_paused) + views.currentCallsInfo.setText(R.string.call_only_paused) } else { - currentCallsInfo.setText(R.string.call_only_active) + views.currentCallsInfo.text = resources.getString(R.string.call_only_active, formattedDuration) } } else { if (heldCalls.size > 1) { - currentCallsInfo.text = resources.getString(R.string.call_only_multiple_paused , heldCalls.size) + views.currentCallsInfo.text = resources.getString(R.string.call_only_multiple_paused , heldCalls.size) } else if (heldCalls.size == 1) { - currentCallsInfo.setText(R.string.call_active_and_single_paused) + views.currentCallsInfo.text = resources.getString(R.string.call_active_and_single_paused, formattedDuration) } else { - currentCallsInfo.text = resources.getString(R.string.call_active_and_multiple_paused, "00:00", heldCalls.size) + views.currentCallsInfo.text = resources.getString(R.string.call_active_and_multiple_paused, formattedDuration, heldCalls.size) } } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt b/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt similarity index 66% rename from vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt rename to vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt index adee15afe3..5de4938cc8 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt @@ -25,34 +25,44 @@ import im.vector.app.features.call.webrtc.WebRtcCall import org.webrtc.RendererCommon import org.webrtc.SurfaceViewRenderer -class ActiveCallViewHolder { +class KnownCallsViewHolder { private var activeCallPiP: SurfaceViewRenderer? = null - private var activeCallView: CurrentCallsView? = null + private var currentCallsView: CurrentCallsView? = null private var pipWrapper: CardView? = null - private var activeCall: WebRtcCall? = null + private var currentCall: WebRtcCall? = null + private var calls: List = emptyList() private var activeCallPipInitialized = false - fun updateCall(activeCall: WebRtcCall?, calls: List) { - this.activeCall = activeCall - val hasActiveCall = activeCall?.mxCall?.state is CallState.Connected + private val tickListener = object : WebRtcCall.Listener { + override fun onTick(formattedDuration: String) { + currentCallsView?.render(calls, formattedDuration) + } + } + + fun updateCall(currentCall: WebRtcCall?, calls: List) { + this.currentCall?.removeListener(tickListener) + this.currentCall = currentCall + this.currentCall?.addListener(tickListener) + this.calls = calls + val hasActiveCall = currentCall?.mxCall?.state is CallState.Connected if (hasActiveCall) { - val isVideoCall = activeCall?.mxCall?.isVideoCall == true + val isVideoCall = currentCall?.mxCall?.isVideoCall == true if (isVideoCall) initIfNeeded() - activeCallView?.isVisible = !isVideoCall - activeCallView?.render(calls) + currentCallsView?.isVisible = !isVideoCall + currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "") pipWrapper?.isVisible = isVideoCall activeCallPiP?.isVisible = isVideoCall activeCallPiP?.let { - activeCall?.attachViewRenderers(null, it, null) + currentCall?.attachViewRenderers(null, it, null) } } else { - activeCallView?.isVisible = false + currentCallsView?.isVisible = false activeCallPiP?.isVisible = false pipWrapper?.isVisible = false activeCallPiP?.let { - activeCall?.detachRenderers(listOf(it)) + currentCall?.detachRenderers(listOf(it)) } } } @@ -72,27 +82,29 @@ class ActiveCallViewHolder { fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: CurrentCallsView, pipWrapper: CardView, interactionListener: CurrentCallsView.Callback) { this.activeCallPiP = activeCallPiP - this.activeCallView = activeCallView + this.currentCallsView = activeCallView this.pipWrapper = pipWrapper - this.activeCallView?.callback = interactionListener + this.currentCallsView?.callback = interactionListener pipWrapper.setOnClickListener( DebouncedClickListener({ _ -> interactionListener.onTapToReturnToCall() }) ) + this.currentCall?.addListener(tickListener) } fun unBind() { activeCallPiP?.let { - activeCall?.detachRenderers(listOf(it)) + currentCall?.detachRenderers(listOf(it)) } if (activeCallPipInitialized) { activeCallPiP?.release() } - this.activeCallView?.callback = null + this.currentCallsView?.callback = null + this.currentCall?.removeListener(tickListener) pipWrapper?.setOnClickListener(null) activeCallPiP = null - activeCallView = null + currentCallsView = null pipWrapper = null } } diff --git a/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt b/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt new file mode 100644 index 0000000000..9d3a6e1b77 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2020 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 im.vector.app.core.utils + +import android.os.Handler +import android.os.SystemClock + +class CountUpTimer(private val intervalInMs: Long) { + + private var startTimestamp: Long = 0 + private var delayTime: Long = 0 + private var lastPauseTimestamp: Long = 0 + private var isRunning: Boolean = false + + var tickListener: TickListener? = null + + private val tickHandler: Handler = Handler() + private val tickSelector = Runnable { + if (isRunning) { + tickListener?.onTick(time) + startTicking() + } + } + + init { + reset() + } + + /** + * Reset the timer, also clears all laps information. Running status will not affected + */ + fun reset() { + startTimestamp = SystemClock.elapsedRealtime() + delayTime = 0 + lastPauseTimestamp = startTimestamp + } + + /** + * Pause the timer + */ + fun pause() { + if (isRunning) { + lastPauseTimestamp = SystemClock.elapsedRealtime() + isRunning = false + stopTicking() + } + } + + /** + * Resume the timer + */ + fun resume() { + if (!isRunning) { + val currentTime: Long = SystemClock.elapsedRealtime() + delayTime += currentTime - lastPauseTimestamp + isRunning = true + startTicking() + } + } + val time: Long + get() = if (isRunning) { + SystemClock.elapsedRealtime() - startTimestamp - delayTime + } else { + lastPauseTimestamp - startTimestamp - delayTime + } + + private fun startTicking() { + tickHandler.removeCallbacksAndMessages(null) + val time = time + val remainingTimeInInterval = intervalInMs - time % intervalInMs + tickHandler.postDelayed(tickSelector, remainingTimeInInterval) + } + + private fun stopTicking() { + tickHandler.removeCallbacksAndMessages(null) + } + + + interface TickListener { + fun onTick(milliseconds: Long) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 5f7bf1802a..ac814f8444 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -50,6 +50,7 @@ import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.parcelize.Parcelize +import okhttp3.internal.concurrent.formatDuration import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCallDetail @@ -205,7 +206,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } } } else { - views.callStatusText.text = null + views.callStatusText.text = state.formattedDuration if (callArgs.isVideoCall) { views.callVideoGroup.isVisible = true views.callInfoGroup.isVisible = false diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 7e3977bc99..c780ec1008 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -79,6 +79,12 @@ class VectorCallViewModel @AssistedInject constructor( } } + override fun onTick(formattedDuration: String) { + setState { + copy(formattedDuration = formattedDuration) + } + } + override fun onStateUpdate(call: MxCall) { val callState = call.state if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { @@ -176,8 +182,9 @@ class VectorCallViewModel @AssistedInject constructor( isLocalOnHold = webRtcCall.isLocalOnHold(), isRemoteOnHold = webRtcCall.remoteOnHold, availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(), - isFrontCamera = call?.currentCameraType() == CameraType.FRONT, - canSwitchCamera = call?.canSwitchCamera() ?: false, + isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT, + canSwitchCamera = webRtcCall.canSwitchCamera(), + formattedDuration = webRtcCall.formattedDuration(), isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD ) } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt index aba109091a..15fa2a37fa 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt @@ -38,7 +38,8 @@ data class VectorCallViewState( val availableSoundDevices: List = emptyList(), val callState: Async = Uninitialized, val otherKnownCallInfo: CallInfo? = null, - val callInfo: CallInfo = CallInfo(callId) + val callInfo: CallInfo = CallInfo(callId), + val formattedDuration: String = "" ) : MvRxState { data class CallInfo( diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 1b7775a5d7..f7cd148618 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -20,6 +20,7 @@ import android.content.Context import android.hardware.camera2.CameraManager import androidx.core.content.getSystemService import im.vector.app.core.services.CallService +import im.vector.app.core.utils.CountUpTimer import im.vector.app.features.call.CallAudioManager import im.vector.app.features.call.CameraEventsHandlerAdapter import im.vector.app.features.call.CameraProxy @@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.call.TurnServerResponse import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent @@ -53,6 +55,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent import org.matrix.android.sdk.api.session.room.model.call.SdpType import org.matrix.android.sdk.internal.util.awaitCallback +import org.threeten.bp.Duration import org.webrtc.AudioSource import org.webrtc.AudioTrack import org.webrtc.Camera1Enumerator @@ -96,6 +99,8 @@ class WebRtcCall(val mxCall: MxCall, fun onCaptureStateChanged() {} fun onCameraChanged() {} fun onHoldUnhold() {} + fun onTick(formattedDuration: String) {} + override fun onStateUpdate(call: MxCall) {} } private val listeners = CopyOnWriteArrayList() @@ -130,6 +135,18 @@ class WebRtcCall(val mxCall: MxCall, private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null + private val timer = CountUpTimer(1000).apply { + tickListener = object : CountUpTimer.TickListener { + override fun onTick(milliseconds: Long) { + val formattedDuration = formatDuration(Duration.ofMillis(milliseconds)) + listeners.forEach { + tryOrNull { it.onTick(formattedDuration) } + } + } + } + } + + // Mute status var micMuted = false private set @@ -209,6 +226,12 @@ class WebRtcCall(val mxCall: MxCall, } } + fun formattedDuration(): String { + return formatDuration( + Duration.ofMillis(timer.time) + ) + } + private fun createPeerConnection(turnServerResponse: TurnServerResponse?) { val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return val iceServers = mutableListOf().apply { @@ -547,7 +570,7 @@ class WebRtcCall(val mxCall: MxCall, return callOnHold } - fun updateRemoteOnHold(onHold: Boolean) = synchronized(this){ + fun updateRemoteOnHold(onHold: Boolean) = synchronized(this) { if (remoteOnHold == onHold) return remoteOnHold = onHold if (!onHold) { @@ -574,11 +597,11 @@ class WebRtcCall(val mxCall: MxCall, updateMuteStatus() } - fun canSwitchCamera(): Boolean = synchronized(this){ + fun canSwitchCamera(): Boolean = synchronized(this) { return availableCamera.size > 1 } - private fun getOppositeCameraIfAny(): CameraProxy? = synchronized(this){ + private fun getOppositeCameraIfAny(): CameraProxy? = synchronized(this) { val currentCamera = cameraInUse ?: return null return if (currentCamera.type == CameraType.FRONT) { availableCamera.firstOrNull { it.type == CameraType.BACK } @@ -587,7 +610,7 @@ class WebRtcCall(val mxCall: MxCall, } } - fun switchCamera() = synchronized(this){ + fun switchCamera() = synchronized(this) { Timber.v("## VOIP switchCamera") if (mxCall.state is CallState.Connected && mxCall.isVideoCall) { val oppositeCamera = getOppositeCameraIfAny() ?: return @@ -630,7 +653,7 @@ class WebRtcCall(val mxCall: MxCall, } } - fun currentCameraType(): CameraType? = synchronized(this){ + fun currentCameraType(): CameraType? = synchronized(this) { return cameraInUse?.type } @@ -638,8 +661,10 @@ class WebRtcCall(val mxCall: MxCall, return currentCaptureFormat } - private fun release() { + private fun release() { mxCall.removeListener(this) + timer.reset() + timer.tickListener = null videoCapturer?.stopCapture() videoCapturer?.dispose() videoCapturer = null @@ -784,6 +809,11 @@ class WebRtcCall(val mxCall: MxCall, } val nowOnHold = isLocalOnHold() if (prevOnHold != nowOnHold) { + if (nowOnHold) { + timer.pause() + } else { + timer.resume() + } listeners.forEach { tryOrNull { it.onHoldUnhold() } } @@ -791,9 +821,26 @@ class WebRtcCall(val mxCall: MxCall, } } + private fun formatDuration(duration: Duration): String { + val hours = duration.seconds / 3600 + val minutes = (duration.seconds % 3600) / 60 + val seconds = duration.seconds % 60 + return if (hours > 0) { + String.format("%d:%02d:%02d", hours, minutes, seconds) + } else { + String.format("%02d:%02d", minutes, seconds) + } + } + // MxCall.StateListener override fun onStateUpdate(call: MxCall) { + val state = call.state + if (state is CallState.Connected && state.iceConnectionState == MxPeerConnectionState.CONNECTED) { + timer.resume() + } else { + timer.pause() + } listeners.forEach { tryOrNull { it.onStateUpdate(call) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 87b561ff93..4c7b7aa991 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -33,7 +33,7 @@ import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.ui.views.CurrentCallsView -import im.vector.app.core.ui.views.ActiveCallViewHolder +import im.vector.app.core.ui.views.KnownCallsViewHolder import im.vector.app.core.ui.views.KeysBackupBanner import im.vector.app.databinding.FragmentHomeDetailBinding import im.vector.app.features.call.SharedKnownCallsViewModel @@ -83,7 +83,7 @@ class HomeDetailFragment @Inject constructor( return FragmentHomeDetailBinding.inflate(inflater, container, false) } - private val activeCallViewHolder = ActiveCallViewHolder() + private val activeCallViewHolder = KnownCallsViewHolder() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 958e956fee..744595ecf0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -90,7 +90,7 @@ import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.ui.views.CurrentCallsView -import im.vector.app.core.ui.views.ActiveCallViewHolder +import im.vector.app.core.ui.views.KnownCallsViewHolder import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.JumpToReadMarkerView import im.vector.app.core.ui.views.NotificationAreaView @@ -295,7 +295,7 @@ class RoomDetailFragment @Inject constructor( private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView private var lockSendButton = false - private val activeCallViewHolder = ActiveCallViewHolder() + private val activeCallViewHolder = KnownCallsViewHolder() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) From a16086db6ff2a48e4b917f28c8859530f2b7056d Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 15 Dec 2020 18:44:59 +0100 Subject: [PATCH 25/57] VoIP: always use silent for pending call notification --- .../vector/app/core/services/CallService.kt | 35 ------------------- .../app/features/call/webrtc/WebRtcCall.kt | 12 ------- .../home/room/detail/RoomDetailViewModel.kt | 1 + .../notifications/NotificationUtils.kt | 11 ++---- .../layout/alerter_incoming_call_layout.xml | 2 ++ 5 files changed, 6 insertions(+), 55 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index efdec1c251..0350971087 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -144,10 +144,6 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe ACTION_CALL_TERMINATED -> { handleCallTerminated(intent) } - ACTION_ONGOING_CALL_BG -> { - // there is an ongoing call but call activity is in background - displayCallOnGoingInBackground(intent) - } else -> { // Should not happen callRingPlayerIncoming?.stop() @@ -278,26 +274,6 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe notificationManager.notify(callId.hashCode(), notification) } - /** - * Display a call in progress notification. - */ - private fun displayCallOnGoingInBackground(intent: Intent) { - Timber.v("## VOIP displayCallInProgressNotification") - val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return - val call = callManager.getCallById(callId) ?: return - if (!knownCalls.contains(callId)) { - Timber.v("Call in in background for unknown call $callId$") - return - } - val opponentMatrixItem = getOpponentMatrixItem(call) - - val notification = notificationUtils.buildPendingCallNotification( - mxCall = call.mxCall, - title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId, - fromBg = true) - notificationManager.notify(callId.hashCode(), notification) - } - fun addConnection(callConnection: CallConnection) { connections[callConnection.callId] = callConnection } @@ -313,7 +289,6 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_OUTGOING_RINGING_CALL" private const val ACTION_CALL_CONNECTING = "im.vector.app.core.services.CallService.ACTION_CALL_CONNECTING" private const val ACTION_ONGOING_CALL = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL" - private const val ACTION_ONGOING_CALL_BG = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL_BG" private const val ACTION_CALL_TERMINATED = "im.vector.app.core.services.CallService.ACTION_CALL_TERMINATED" private const val ACTION_NO_ACTIVE_CALL = "im.vector.app.core.services.CallService.NO_ACTIVE_CALL" // private const val ACTION_ACTIVITY_VISIBLE = "im.vector.app.core.services.CallService.ACTION_ACTIVITY_VISIBLE" @@ -334,16 +309,6 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe ContextCompat.startForegroundService(context, intent) } - fun onOnGoingCallBackground(context: Context, - callId: String) { - val intent = Intent(context, CallService::class.java) - .apply { - action = ACTION_ONGOING_CALL_BG - putExtra(EXTRA_CALL_ID, callId) - } - - ContextCompat.startForegroundService(context, intent) - } fun onOutgoingCallRinging(context: Context, callId: String) { diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index f7cd148618..e6ebdaf572 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -323,18 +323,6 @@ class WebRtcCall(val mxCall: MxCall, remoteVideoTrack?.removeSink(it) } } - if (remoteSurfaceRenderers.isEmpty()) { - // The call is going to continue in background, so ensure notification is visible - mxCall - .takeIf { it.state is CallState.Connected } - ?.let { mxCall -> - // Start background service with notification - CallService.onOnGoingCallBackground( - context = context, - callId = mxCall.callId - ) - } - } } private suspend fun setupOutgoingCall() = withContext(dispatcher) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index ab4236489b..8dff6fe675 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -1323,6 +1323,7 @@ class RoomDetailViewModel @AssistedInject constructor( } } .subscribe { + Timber.v("Unread state: $it") setState { copy(unreadState = it) } } .disposeOnClear() diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 2ec917c739..8850507c6a 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -393,9 +393,9 @@ class NotificationUtils @Inject constructor(private val context: Context, */ @SuppressLint("NewApi") fun buildPendingCallNotification(mxCall: MxCall, - title: String, - fromBg: Boolean = false): Notification { - val builder = NotificationCompat.Builder(context, if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID) + title: String): Notification { + + val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) .setContentTitle(ensureTitleNotEmpty(title)) .apply { if (mxCall.isVideoCall) { @@ -407,11 +407,6 @@ class NotificationUtils @Inject constructor(private val context: Context, .setSmallIcon(R.drawable.incoming_call_notification_transparent) .setCategory(NotificationCompat.CATEGORY_CALL) - if (fromBg) { - builder.priority = NotificationCompat.PRIORITY_LOW - builder.setOngoing(true) - } - val rejectCallPendingIntent = buildRejectCallPendingIntent(mxCall.callId) builder.addAction( diff --git a/vector/src/main/res/layout/alerter_incoming_call_layout.xml b/vector/src/main/res/layout/alerter_incoming_call_layout.xml index 6cbcfd10d7..a874e41743 100644 --- a/vector/src/main/res/layout/alerter_incoming_call_layout.xml +++ b/vector/src/main/res/layout/alerter_incoming_call_layout.xml @@ -4,6 +4,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" + android:paddingTop="4dp" + android:paddingBottom="4dp" xmlns:tools="http://schemas.android.com/tools" tools:style="@style/AlertStyle" xmlns:app="http://schemas.android.com/apk/res-auto"> From c53111a85a96e7069f21b12f8b2559ebe8d35eef Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 22 Dec 2020 12:30:48 +0100 Subject: [PATCH 26/57] VoIP: fix bunch of issues --- .../app/core/ui/views/CurrentCallsView.kt | 2 +- .../app/core/ui/views/KnownCallsViewHolder.kt | 3 + .../im/vector/app/core/utils/CountUpTimer.kt | 81 +++++----------- .../app/features/call/VectorCallActivity.kt | 16 ++-- .../app/features/call/VectorCallViewModel.kt | 4 +- .../app/features/call/webrtc/WebRtcCall.kt | 96 ++++++++++--------- .../notifications/NotificationUtils.kt | 13 +-- 7 files changed, 90 insertions(+), 125 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt index 16cb7785a7..dfcd4629c6 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt @@ -52,7 +52,7 @@ class CurrentCallsView @JvmOverloads constructor( it.mxCall.state is CallState.Connected } val heldCalls = connectedCalls.filter { - it.isLocalOnHold() || it.remoteOnHold + it.isLocalOnHold || it.remoteOnHold } if (connectedCalls.size == 1) { if (heldCalls.size == 1) { diff --git a/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt b/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt index 5de4938cc8..3bd9ce713d 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt @@ -42,6 +42,9 @@ class KnownCallsViewHolder { } fun updateCall(currentCall: WebRtcCall?, calls: List) { + activeCallPiP?.let { + this.currentCall?.detachRenderers(listOf(it)) + } this.currentCall?.removeListener(tickListener) this.currentCall = currentCall this.currentCall?.addListener(tickListener) diff --git a/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt b/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt index 9d3a6e1b77..5b5a406194 100644 --- a/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt +++ b/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt @@ -16,82 +16,45 @@ package im.vector.app.core.utils -import android.os.Handler -import android.os.SystemClock +import io.reactivex.Flowable +import io.reactivex.Observable +import io.reactivex.disposables.Disposable +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong class CountUpTimer(private val intervalInMs: Long) { - private var startTimestamp: Long = 0 - private var delayTime: Long = 0 - private var lastPauseTimestamp: Long = 0 - private var isRunning: Boolean = false + private val elapsedTime: AtomicLong = AtomicLong() + private val resumed: AtomicBoolean = AtomicBoolean(false) + + private val disposable = Observable.interval(intervalInMs, TimeUnit.MILLISECONDS) + .filter { _ -> resumed.get() } + .doOnNext { _ -> elapsedTime.addAndGet(intervalInMs) } + .subscribe { + tickListener?.onTick(elapsedTime.get()) + } var tickListener: TickListener? = null - private val tickHandler: Handler = Handler() - private val tickSelector = Runnable { - if (isRunning) { - tickListener?.onTick(time) - startTicking() - } + fun elapsedTime(): Long{ + return elapsedTime.get() } - init { - reset() - } - - /** - * Reset the timer, also clears all laps information. Running status will not affected - */ - fun reset() { - startTimestamp = SystemClock.elapsedRealtime() - delayTime = 0 - lastPauseTimestamp = startTimestamp - } - - /** - * Pause the timer - */ fun pause() { - if (isRunning) { - lastPauseTimestamp = SystemClock.elapsedRealtime() - isRunning = false - stopTicking() - } + resumed.set(false) } - /** - * Resume the timer - */ fun resume() { - if (!isRunning) { - val currentTime: Long = SystemClock.elapsedRealtime() - delayTime += currentTime - lastPauseTimestamp - isRunning = true - startTicking() - } - } - val time: Long - get() = if (isRunning) { - SystemClock.elapsedRealtime() - startTimestamp - delayTime - } else { - lastPauseTimestamp - startTimestamp - delayTime - } - - private fun startTicking() { - tickHandler.removeCallbacksAndMessages(null) - val time = time - val remainingTimeInInterval = intervalInMs - time % intervalInMs - tickHandler.postDelayed(tickSelector, remainingTimeInInterval) + resumed.set(true) } - private fun stopTicking() { - tickHandler.removeCallbacksAndMessages(null) + fun stop() { + disposable.dispose() } - interface TickListener { fun onTick(milliseconds: Long) } -} \ No newline at end of file +} diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index ac814f8444..9de0dbb7ed 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -50,7 +50,6 @@ import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.parcelize.Parcelize -import okhttp3.internal.concurrent.formatDuration import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCallDetail @@ -166,7 +165,8 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.smallIsHeldIcon.isVisible = false when (callState) { is CallState.Idle, - is CallState.Dialing -> { + is CallState.CreateOffer, + is CallState.Dialing -> { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true views.callStatusText.setText(R.string.call_ring) @@ -187,9 +187,9 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callConnectingProgress.isVisible = true configureCallInfo(state) } - is CallState.Connected -> { + is CallState.Connected -> { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { - if (state.isLocalOnHold) { + if (state.isLocalOnHold || state.isRemoteOnHold) { views.smallIsHeldIcon.isVisible = true views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true @@ -226,13 +226,11 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callStatusText.setText(R.string.call_connecting) views.callConnectingProgress.isVisible = true } - // ensure all attached? - callManager.getCallById(callArgs.callId)?.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, null) } - is CallState.Terminated -> { + is CallState.Terminated -> { finish() } - null -> { + null -> { } } } @@ -255,7 +253,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) avatarRenderer.renderBlur(state.otherKnownCallInfo.otherUserItem, views.otherKnownCallAvatarView, sampling = 20, rounded = false, colorFilter = colorFilter) views.otherKnownCallLayout.isVisible = true - views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold() || it.remoteOnHold }.orFalse() + views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.remoteOnHold }.orFalse() } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index c780ec1008..ac45cdab5a 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -55,7 +55,7 @@ class VectorCallViewModel @AssistedInject constructor( override fun onHoldUnhold() { setState { copy( - isLocalOnHold = call?.isLocalOnHold() ?: false, + isLocalOnHold = call?.isLocalOnHold ?: false, isRemoteOnHold = call?.remoteOnHold ?: false ) } @@ -179,7 +179,7 @@ class VectorCallViewModel @AssistedInject constructor( callState = Success(webRtcCall.mxCall.state), callInfo = VectorCallViewState.CallInfo(callId, item), soundDevice = currentSoundDevice, - isLocalOnHold = webRtcCall.isLocalOnHold(), + isLocalOnHold = webRtcCall.isLocalOnHold, isRemoteOnHold = webRtcCall.remoteOnHold, availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(), isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT, diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index e6ebdaf572..41a43dd924 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -135,7 +135,7 @@ class WebRtcCall(val mxCall: MxCall, private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null - private val timer = CountUpTimer(1000).apply { + private val timer = CountUpTimer(Duration.ofSeconds(1).toMillis()).apply { tickListener = object : CountUpTimer.TickListener { override fun onTick(milliseconds: Long) { val formattedDuration = formatDuration(Duration.ofMillis(milliseconds)) @@ -146,7 +146,6 @@ class WebRtcCall(val mxCall: MxCall, } } - // Mute status var micMuted = false private set @@ -154,6 +153,11 @@ class WebRtcCall(val mxCall: MxCall, private set var remoteOnHold = false private set + var isLocalOnHold = false + private set + + // This value is used to track localOnHold when changing remoteOnHold value + private var wasLocalOnHold = false var offerSdp: CallInviteContent.Offer? = null @@ -228,7 +232,7 @@ class WebRtcCall(val mxCall: MxCall, fun formattedDuration(): String { return formatDuration( - Duration.ofMillis(timer.time) + Duration.ofMillis(timer.elapsedTime()) ) } @@ -257,21 +261,9 @@ class WebRtcCall(val mxCall: MxCall, fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) { Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer") -// this.localSurfaceRenderer = WeakReference(localViewRenderer) -// this.remoteSurfaceRenderer = WeakReference(remoteViewRenderer) localSurfaceRenderers.addIfNeeded(localViewRenderer) remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer) - // The call is going to resume from background, we can reduce notif - mxCall - .takeIf { it.state is CallState.Connected } - ?.let { mxCall -> - // Start background service with notification - CallService.onPendingCall( - context = context, - callId = mxCall.callId) - } - GlobalScope.launch(dispatcher) { when (mode) { VectorCallActivity.INCOMING_ACCEPT -> { @@ -301,9 +293,8 @@ class WebRtcCall(val mxCall: MxCall, } } - fun detachRenderers(renderers: List?) = synchronized(this) { + fun detachRenderers(renderers: List?) { Timber.v("## VOIP detachRenderers") - // currentCall?.localMediaStream?.let { currentCall?.peerConnection?.removeStream(it) } if (renderers.isNullOrEmpty()) { // remove all sinks localSurfaceRenderers.forEach { @@ -545,7 +536,7 @@ class WebRtcCall(val mxCall: MxCall, * rather than 'sendonly') * @returns true if the other party has put us on hold */ - fun isLocalOnHold(): Boolean = synchronized(this) { + private fun computeIsLocalOnHold(): Boolean { if (mxCall.state !is CallState.Connected) return false var callOnHold = true // We consider a call to be on hold only if *all* the tracks are on hold @@ -558,38 +549,46 @@ class WebRtcCall(val mxCall: MxCall, return callOnHold } - fun updateRemoteOnHold(onHold: Boolean) = synchronized(this) { - if (remoteOnHold == onHold) return - remoteOnHold = onHold - if (!onHold) { - onCallBecomeActive(this) + fun updateRemoteOnHold(onHold: Boolean) { + GlobalScope.launch(dispatcher) { + if (remoteOnHold == onHold) return@launch + val direction: RtpTransceiver.RtpTransceiverDirection + if (onHold) { + wasLocalOnHold = isLocalOnHold + remoteOnHold = true + isLocalOnHold = true + direction = RtpTransceiver.RtpTransceiverDirection.INACTIVE + } else { + remoteOnHold = false + isLocalOnHold = wasLocalOnHold + onCallBecomeActive(this@WebRtcCall) + direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV + } + for (transceiver in peerConnection?.transceivers ?: emptyList()) { + transceiver.direction = direction + } + updateMuteStatus() + listeners.forEach { + tryOrNull { it.onHoldUnhold() } + } } - val direction = if (onHold) { - RtpTransceiver.RtpTransceiverDirection.INACTIVE - } else { - RtpTransceiver.RtpTransceiverDirection.SEND_RECV - } - for (transceiver in peerConnection?.transceivers ?: emptyList()) { - transceiver.direction = direction - } - updateMuteStatus() } - fun muteCall(muted: Boolean) = synchronized(this) { + fun muteCall(muted: Boolean) { micMuted = muted updateMuteStatus() } - fun enableVideo(enabled: Boolean) = synchronized(this) { + fun enableVideo(enabled: Boolean) { videoMuted = !enabled updateMuteStatus() } - fun canSwitchCamera(): Boolean = synchronized(this) { + fun canSwitchCamera(): Boolean { return availableCamera.size > 1 } - private fun getOppositeCameraIfAny(): CameraProxy? = synchronized(this) { + private fun getOppositeCameraIfAny(): CameraProxy? { val currentCamera = cameraInUse ?: return null return if (currentCamera.type == CameraType.FRONT) { availableCamera.firstOrNull { it.type == CameraType.BACK } @@ -598,7 +597,7 @@ class WebRtcCall(val mxCall: MxCall, } } - fun switchCamera() = synchronized(this) { + fun switchCamera() { Timber.v("## VOIP switchCamera") if (mxCall.state is CallState.Connected && mxCall.isVideoCall) { val oppositeCamera = getOppositeCameraIfAny() ?: return @@ -641,17 +640,18 @@ class WebRtcCall(val mxCall: MxCall, } } - fun currentCameraType(): CameraType? = synchronized(this) { + fun currentCameraType(): CameraType? { return cameraInUse?.type } - fun currentCaptureFormat(): CaptureFormat = synchronized(this) { + fun currentCaptureFormat(): CaptureFormat { return currentCaptureFormat } private fun release() { + listeners.clear() mxCall.removeListener(this) - timer.reset() + timer.stop() timer.tickListener = null videoCapturer?.stopCapture() videoCapturer?.dispose() @@ -703,10 +703,11 @@ class WebRtcCall(val mxCall: MxCall, } } - fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) = synchronized(this) { + fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) { if (mxCall.state == CallState.Terminated) { return } + val wasConnected = mxCall.state is CallState.Connected mxCall.state = CallState.Terminated // Close tracks ASAP localVideoTrack?.setEnabled(false) @@ -715,12 +716,13 @@ class WebRtcCall(val mxCall: MxCall, val cameraManager = context.getSystemService()!! cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) } - release() - listeners.clear() + GlobalScope.launch(dispatcher) { + release() + } onCallEnded(this) if (originatedByMe) { // send hang up event - if (mxCall.state is CallState.Connected) { + if (wasConnected) { mxCall.hangUp(reason) } else { mxCall.reject() @@ -783,7 +785,7 @@ class WebRtcCall(val mxCall: MxCall, Timber.i("Ignoring colliding negotiate event because we're impolite") return@launch } - val prevOnHold = isLocalOnHold() + val prevOnHold = computeIsLocalOnHold() try { val sdp = SessionDescription(type.asWebRTC(), sdpText) peerConnection.awaitSetRemoteDescription(sdp) @@ -795,8 +797,10 @@ class WebRtcCall(val mxCall: MxCall, } catch (failure: Throwable) { Timber.e(failure, "Failed to complete negotiation") } - val nowOnHold = isLocalOnHold() + val nowOnHold = computeIsLocalOnHold() + wasLocalOnHold = nowOnHold if (prevOnHold != nowOnHold) { + isLocalOnHold = nowOnHold if (nowOnHold) { timer.pause() } else { diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 8850507c6a..48f0c54e76 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -297,11 +297,6 @@ class NotificationUtils @Inject constructor(private val context: Context, .setLights(accentColor, 500, 500) .setOngoing(true) - // Compat: Display the incoming call notification on the lock screen - builder.priority = NotificationCompat.PRIORITY_HIGH - - // -// val pendingIntent = stackBuilder.getPendingIntent(requestId, PendingIntent.FLAG_UPDATE_CURRENT) val contentIntent = VectorCallActivity.newIntent( context = context, @@ -340,9 +335,11 @@ class NotificationUtils @Inject constructor(private val context: Context, answerCallPendingIntent ) ) - - builder.setFullScreenIntent(contentPendingIntent, true) - + if (fromBg) { + // Compat: Display the incoming call notification on the lock screen + builder.priority = NotificationCompat.PRIORITY_HIGH + builder.setFullScreenIntent(contentPendingIntent, true) + } return builder.build() } From 8734101d8774d7504103fd7a03b6cb9589f16d7f Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 22 Dec 2020 16:44:02 +0100 Subject: [PATCH 27/57] Platform: fix RoomSummaryHolder usage (temporary) --- .../api/session/room/timeline/TimelineEvent.kt | 2 ++ .../im/vector/app/core/di/VectorComponent.kt | 3 +++ .../home/room/detail/RoomDetailViewModel.kt | 2 +- .../detail/timeline/TimelineEventController.kt | 3 +-- .../timeline/action/MessageActionsViewModel.kt | 2 +- .../detail/timeline/factory/CallItemFactory.kt | 8 +++++++- .../factory/MergedHeaderItemFactory.kt | 11 +++++------ .../timeline/factory/MessageItemFactory.kt | 17 ++++++++++------- .../timeline/factory/NoticeItemFactory.kt | 4 +--- .../timeline/factory/RoomCreateItemFactory.kt | 3 +-- .../timeline/factory/TimelineItemFactory.kt | 4 ++-- .../factory/VerificationItemFactory.kt | 3 +-- .../format/DisplayableEventFormatter.kt | 5 ++--- .../timeline/format/NoticeEventFormatter.kt | 5 ++++- .../helper/MessageInformationDataFactory.kt | 2 +- .../timeline/helper/RoomSummaryHolder.kt | 18 +++++++++++------- .../home/room/list/RoomSummaryItemFactory.kt | 2 +- .../notifications/NotifiableEventResolver.kt | 4 ++-- 18 files changed, 56 insertions(+), 42 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 73cb94b417..4216b03a87 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -52,6 +52,8 @@ data class TimelineEvent( } } + val roomId = root.roomId ?: "" + val metadata = HashMap() /** diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index fb8b1eac07..433efab923 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -38,6 +38,7 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.HomeRoomListDataSource import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider +import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.login.ReAuthHelper @@ -158,6 +159,8 @@ interface VectorComponent { fun webRtcCallManager(): WebRtcCallManager + fun roomSummaryHolder(): RoomSummaryHolder + @Component.Factory interface Factory { fun create(@BindsInstance context: Context): VectorComponent diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 8dff6fe675..18018b3fe6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -1421,7 +1421,7 @@ class RoomDetailViewModel @AssistedInject constructor( } override fun onCleared() { - roomSummaryHolder.clear() + roomSummaryHolder.remove(room.roomId) timeline.dispose() timeline.removeAllListeners() if (vectorPreferences.sendTypingNotifs()) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index dd234266b1..018b8691c0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -35,7 +35,6 @@ import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailViewState import im.vector.app.features.home.room.detail.UnreadState import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItemFactory -import im.vector.app.features.home.room.detail.timeline.factory.NoticeItemFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder @@ -77,7 +76,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val mergedHeaderItemFactory: MergedHeaderItemFactory, private val session: Session, private val callManager: WebRtcCallManager, - private val noticeItemFactory: NoticeItemFactory, @TimelineEventControllerHandler private val backgroundHandler: Handler ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { @@ -104,6 +102,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // TODO move all callbacks to this? fun onTimelineItemAction(itemAction: RoomDetailAction) + // Introduce ViewModel scoped component (or Hilt?) fun getPreviewUrlRetriever(): PreviewUrlRetriever } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 716fdca2ad..3684f19b61 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -196,7 +196,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted EventType.CALL_CANDIDATES, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> { - noticeEventFormatter.format(timelineEvent, room?.roomSummary()) + noticeEventFormatter.format(timelineEvent) } else -> null } ?: "" diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt index 8c972a8684..ea91b767ab 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt @@ -52,6 +52,7 @@ class CallItemFactory @Inject constructor( callback: TimelineEventController.Callback? ): VectorEpoxyModel<*>? { if (event.root.eventId == null) return null + val roomId = event.roomId val informationData = messageInformationDataFactory.create(event, null) val callSignalingContent = event.getCallSignallingContent() ?: return null val callId = callSignalingContent.callId ?: return null @@ -64,6 +65,7 @@ class CallItemFactory @Inject constructor( return when (event.root.getClearType()) { EventType.CALL_ANSWER -> { createCallTileTimelineItem( + roomId = roomId, callId = callId, callStatus = CallTileTimelineItem.CallStatus.IN_CALL, callKind = callKind, @@ -75,6 +77,7 @@ class CallItemFactory @Inject constructor( } EventType.CALL_INVITE -> { createCallTileTimelineItem( + roomId = roomId, callId = callId, callStatus = CallTileTimelineItem.CallStatus.INVITED, callKind = callKind, @@ -86,6 +89,7 @@ class CallItemFactory @Inject constructor( } EventType.CALL_REJECT -> { createCallTileTimelineItem( + roomId = roomId, callId = callId, callStatus = CallTileTimelineItem.CallStatus.REJECTED, callKind = callKind, @@ -97,6 +101,7 @@ class CallItemFactory @Inject constructor( } EventType.CALL_HANGUP -> { createCallTileTimelineItem( + roomId = roomId, callId = callId, callStatus = CallTileTimelineItem.CallStatus.ENDED, callKind = callKind, @@ -121,6 +126,7 @@ class CallItemFactory @Inject constructor( } private fun createCallTileTimelineItem( + roomId: String, callId: String, callKind: CallTileTimelineItem.CallKind, callStatus: CallTileTimelineItem.CallStatus, @@ -129,7 +135,7 @@ class CallItemFactory @Inject constructor( isStillActive: Boolean, callback: TimelineEventController.Callback? ): CallTileTimelineItem? { - val userOfInterest = roomSummaryHolder.roomSummary?.toMatrixItem() ?: return null + val userOfInterest = roomSummaryHolder.get(roomId)?.toMatrixItem() ?: return null val attributes = messageItemAttributesFactory.create(null, informationData, callback).let { CallTileTimelineItem.Attributes( callId = callId, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 23bd041e95..7846e2970e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -77,7 +77,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde } } - private fun isDirectRoom() = roomSummaryHolder.roomSummary?.isDirect.orFalse() + private fun isDirectRoom(roomId: String) = roomSummaryHolder.get(roomId)?.isDirect.orFalse() private fun buildMembershipEventsMergedSummary(currentPosition: Int, items: List, @@ -102,7 +102,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde memberName = mergedEvent.senderInfo.disambiguatedDisplayName, localId = mergedEvent.localId, eventId = mergedEvent.root.eventId ?: "", - isDirectRoom = isDirectRoom() + isDirectRoom = isDirectRoom(event.roomId) ) mergedData.add(data) } @@ -174,7 +174,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde memberName = mergedEvent.senderInfo.disambiguatedDisplayName, localId = mergedEvent.localId, eventId = mergedEvent.root.eventId ?: "", - isDirectRoom = isDirectRoom() + isDirectRoom = isDirectRoom(event.roomId) ) mergedData.add(data) } @@ -191,8 +191,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde collapsedEventIds.removeAll(mergedEventIds) } val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } - val powerLevelsHelper = roomSummaryHolder.roomSummary?.roomId - ?.let { activeSessionHolder.getSafeActiveSession()?.getRoom(it) } + val powerLevelsHelper = activeSessionHolder.getSafeActiveSession()?.getRoom(event.roomId) ?.let { it.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)?.content?.toModel() } ?.let { PowerLevelsHelper(it) } val currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: "" @@ -209,7 +208,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde readReceiptsCallback = callback, callback = callback, currentUserId = currentUserId, - roomSummary = roomSummaryHolder.roomSummary, + roomSummary = roomSummaryHolder.get(event.roomId), canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false, canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false, canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index a1e041b98f..d6473205a6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -16,6 +16,8 @@ package im.vector.app.features.home.room.detail.timeline.factory +import android.content.Intent +import android.os.Parcelable import android.text.SpannableStringBuilder import android.text.Spanned import android.text.TextPaint @@ -105,15 +107,17 @@ class MessageItemFactory @Inject constructor( private val messageItemAttributesFactory: MessageItemAttributesFactory, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder, - private val roomSummaryHolder: RoomSummaryHolder, private val defaultItemFactory: DefaultItemFactory, private val noticeItemFactory: NoticeItemFactory, private val avatarSizeProvider: AvatarSizeProvider, private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val session: Session) { + // TODO inject this properly? + private var roomId: String = "" + private val pillsPostProcessor by lazy { - pillsPostProcessorFactory.create(roomSummaryHolder.roomSummary?.roomId) + pillsPostProcessorFactory.create(roomId) } fun create(event: TimelineEvent, @@ -122,8 +126,8 @@ class MessageItemFactory @Inject constructor( callback: TimelineEventController.Callback? ): VectorEpoxyModel<*>? { event.root.eventId ?: return null + roomId = event.roomId val informationData = messageInformationDataFactory.create(event, nextEvent) - if (event.root.isRedacted()) { // message is redacted val attributes = messageItemAttributesFactory.create(null, informationData, callback) @@ -139,7 +143,7 @@ class MessageItemFactory @Inject constructor( || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should display it when debugging as a notice event - return noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + return noticeItemFactory.create(event, highlight, callback) } val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback) @@ -155,7 +159,7 @@ class MessageItemFactory @Inject constructor( is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } } @@ -229,14 +233,13 @@ class MessageItemFactory @Inject constructor( attributes: AbsMessageItem.Attributes): VerificationRequestItem? { // If this request is not sent by me or sent to me, we should ignore it in timeline val myUserId = session.myUserId - val roomId = roomSummaryHolder.roomSummary?.roomId if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) { return null } val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId val otherUserName = if (informationData.sentByMe) { - session.getRoomMember(messageContent.toUserId, roomId ?: "")?.displayName + session.getRoomMember(messageContent.toUserId, roomId)?.displayName } else { informationData.memberName } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index ec065543f5..cd8c682f39 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -24,7 +24,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvide import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.item.NoticeItem import im.vector.app.features.home.room.detail.timeline.item.NoticeItem_ -import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject @@ -35,9 +34,8 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv fun create(event: TimelineEvent, highlight: Boolean, - roomSummary: RoomSummary?, callback: TimelineEventController.Callback?): NoticeItem? { - val formattedText = eventFormatter.format(event, roomSummary) ?: return null + val formattedText = eventFormatter.format(event) ?: return null val informationData = informationDataFactory.create(event, null) val attributes = NoticeItem.Attributes( avatarRenderer = avatarRenderer, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt index 25b5fd718b..4c2029e347 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt @@ -33,7 +33,6 @@ import javax.inject.Inject class RoomCreateItemFactory @Inject constructor(private val stringProvider: StringProvider, private val userPreferencesProvider: UserPreferencesProvider, private val session: Session, - private val roomSummaryHolder: RoomSummaryHolder, private val noticeItemFactory: NoticeItemFactory) { fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { @@ -54,7 +53,7 @@ class RoomCreateItemFactory @Inject constructor(private val stringProvider: Stri private fun defaultRendering(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { return if (userPreferencesProvider.shouldShowHiddenEvents()) { - noticeItemFactory.create(event, false, roomSummaryHolder.roomSummary, callback) + noticeItemFactory.create(event, false, callback) } else { null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 432736dba0..ffef5a26bc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -63,7 +63,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_POWER_LEVELS, EventType.REACTION, - EventType.REDACTION -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback) // State room create EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) @@ -91,7 +91,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me // TODO These are not filtered out by timeline when encrypted // For now manually ignore if (userPreferencesProvider.shouldShowHiddenEvents()) { - noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + noticeItemFactory.create(event, highlight, callback) } else { null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt index 59daf5a0a0..aadc1c1c38 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt @@ -51,7 +51,6 @@ class VerificationItemFactory @Inject constructor( private val avatarSizeProvider: AvatarSizeProvider, private val noticeItemFactory: NoticeItemFactory, private val userPreferencesProvider: UserPreferencesProvider, - private val roomSummaryHolder: RoomSummaryHolder, private val stringProvider: StringProvider, private val session: Session ) { @@ -153,7 +152,7 @@ class VerificationItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback? ): VectorEpoxyModel<*>? { - if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback) return null } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index f4632b0e10..499e27f838 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -23,7 +23,6 @@ import im.vector.app.core.resources.StringProvider import me.gujun.android.span.span import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS @@ -41,7 +40,7 @@ class DisplayableEventFormatter @Inject constructor( private val noticeEventFormatter: NoticeEventFormatter ) { - fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean, roomSummary: RoomSummary?): CharSequence { + fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean): CharSequence { if (timelineEvent.root.isRedacted()) { return noticeEventFormatter.formatRedactedEvent(timelineEvent.root) } @@ -131,7 +130,7 @@ class DisplayableEventFormatter @Inject constructor( } else -> { return span { - text = noticeEventFormatter.format(timelineEvent, roomSummary) ?: "" + text = noticeEventFormatter.format(timelineEvent) ?: "" textStyle = "italic" } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index c725f5b7dc..a05b6a7ff3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.format import im.vector.app.ActiveSessionDataSource import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.extensions.appendNl import org.matrix.android.sdk.api.extensions.orFalse @@ -55,6 +56,7 @@ class NoticeEventFormatter @Inject constructor( private val activeSessionDataSource: ActiveSessionDataSource, private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, private val vectorPreferences: VectorPreferences, + private val roomSummaryHolder: RoomSummaryHolder, private val sp: StringProvider ) { @@ -65,7 +67,8 @@ class NoticeEventFormatter @Inject constructor( private fun RoomSummary?.isDm() = this?.isDirect.orFalse() - fun format(timelineEvent: TimelineEvent, rs: RoomSummary?): CharSequence? { + fun format(timelineEvent: TimelineEvent): CharSequence? { + val rs = roomSummaryHolder.get(timelineEvent.roomId) return when (val type = timelineEvent.root.getClearType()) { EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs) EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, rs) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 8a8bf364e1..cbc1427197 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -116,7 +116,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses } private fun getE2EDecoration(event: TimelineEvent): E2EDecoration { - val roomSummary = roomSummaryHolder.roomSummary + val roomSummary = roomSummaryHolder.get(event.roomId) return if ( event.root.sendState == SendState.SYNCED && roomSummary?.isEncrypted.orFalse() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummaryHolder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummaryHolder.kt index d3ae091733..ce1a7f3c3d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummaryHolder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummaryHolder.kt @@ -19,22 +19,26 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.di.ScreenScope import org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject +import javax.inject.Singleton /* - This holds an instance of the current room summary. - You should use this in the context of the timeline. + You can use this to share room summary instances within the app. + You should probably use this only in the context of the timeline */ -@ScreenScope +@Singleton class RoomSummaryHolder @Inject constructor() { - var roomSummary: RoomSummary? = null - private set + private var roomSummaries = HashMap() fun set(roomSummary: RoomSummary) { - this.roomSummary = roomSummary + roomSummaries[roomSummary.roomId] = roomSummary } + fun get(roomId: String) = roomSummaries[roomId] + + fun remove(roomId: String)= roomSummaries.remove(roomId) + fun clear() { - roomSummary = null + roomSummaries.clear() } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index 7bb9940ec4..06cb0172d0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -86,7 +86,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor var latestEventTime: CharSequence = "" val latestEvent = roomSummary.latestPreviewableEvent if (latestEvent != null) { - latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not(), roomSummary) + latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not()) latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST) } val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index 9c2dc9b26d..c1bb1dde36 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -91,7 +91,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St if (room == null) { Timber.e("## Unable to resolve room for eventId [$event]") // Ok room is not known in store, but we can still display something - val body = displayableEventFormatter.format(event, false, null) + val body = displayableEventFormatter.format(event, false) val roomName = stringProvider.getString(R.string.notification_unknown_room_name) val senderDisplayName = event.senderInfo.disambiguatedDisplayName @@ -124,7 +124,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St } } - val body = displayableEventFormatter.format(event, false, room.roomSummary()).toString() + val body = displayableEventFormatter.format(event, false).toString() val roomName = room.roomSummary()?.displayName ?: "" val senderDisplayName = event.senderInfo.disambiguatedDisplayName From 33047b5f644cf12e48d7f73999ff644d1404a2de Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 16 Dec 2020 18:49:29 +0100 Subject: [PATCH 28/57] VoIP: fix some other issues --- .../app/features/call/VectorCallViewModel.kt | 9 ++++++--- .../app/features/call/webrtc/WebRtcCall.kt | 4 +--- .../app/features/popup/IncomingCallAlert.kt | 19 +++++++++++++------ .../layout/alerter_incoming_call_layout.xml | 3 +++ 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index ac45cdab5a..6f5a9213ae 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -123,7 +123,11 @@ class VectorCallViewModel @AssistedInject constructor( private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { override fun onCurrentCallChange(call: WebRtcCall?) { - updateOtherKnownCall(call) + if (call == null) { + _viewEvents.post(VectorCallViewEvents.DismissNoCall) + } else { + updateOtherKnownCall(call) + } } override fun onAudioDevicesChange() { @@ -143,8 +147,7 @@ class VectorCallViewModel @AssistedInject constructor( } } - private fun updateOtherKnownCall(currentCall: WebRtcCall?) { - if (currentCall == null) return + private fun updateOtherKnownCall(currentCall: WebRtcCall) { val otherCall = callManager.getCalls().firstOrNull { it.callId != currentCall.callId && it.mxCall.state is CallState.Connected } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 41a43dd924..68cbc67b67 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -707,7 +707,6 @@ class WebRtcCall(val mxCall: MxCall, if (mxCall.state == CallState.Terminated) { return } - val wasConnected = mxCall.state is CallState.Connected mxCall.state = CallState.Terminated // Close tracks ASAP localVideoTrack?.setEnabled(false) @@ -721,8 +720,7 @@ class WebRtcCall(val mxCall: MxCall, } onCallEnded(this) if (originatedByMe) { - // send hang up event - if (wasConnected) { + if (mxCall.state is CallState.Connected || mxCall.isOutgoing) { mxCall.hangUp(reason) } else { mxCall.reject() diff --git a/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt b/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt index b3882945b4..f80d6d1bbd 100644 --- a/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt +++ b/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt @@ -21,6 +21,7 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import im.vector.app.R +import im.vector.app.core.extensions.setLeftDrawable import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem @@ -43,18 +44,24 @@ class IncomingCallAlert(uid: String, : VectorAlert.ViewBinder { override fun bind(view: View) { - val callKind = if (isVideoCall) { - R.string.action_video_call + val (callKindText, callKindIcon) = if (isVideoCall) { + Pair(R.string.action_video_call, R.drawable.ic_call_video_small) } else { - R.string.action_voice_call + Pair(R.string.action_voice_call, R.drawable.ic_call_audio_small) + } + view.findViewById(R.id.incomingCallKindView).apply { + setText(callKindText) + setLeftDrawable(callKindIcon) } - view.findViewById(R.id.incomingCallKindView).setText(callKind) view.findViewById(R.id.incomingCallNameView).text = matrixItem?.getBestName() view.findViewById(R.id.incomingCallAvatar)?.let { imageView -> matrixItem?.let { avatarRenderer.render(it, imageView, GlideApp.with(view.context.applicationContext)) } } - view.findViewById(R.id.incomingCallAcceptView).setOnClickListener { - onAccept() + view.findViewById(R.id.incomingCallAcceptView).apply { + setOnClickListener { + onAccept() + } + setImageResource(callKindIcon) } view.findViewById(R.id.incomingCallRejectView).setOnClickListener { onReject() diff --git a/vector/src/main/res/layout/alerter_incoming_call_layout.xml b/vector/src/main/res/layout/alerter_incoming_call_layout.xml index a874e41743..8487d8885f 100644 --- a/vector/src/main/res/layout/alerter_incoming_call_layout.xml +++ b/vector/src/main/res/layout/alerter_incoming_call_layout.xml @@ -45,6 +45,8 @@ android:layout_marginEnd="8dp" android:ellipsize="end" android:textColor="?riotx_text_secondary" + app:drawableTint="?riotx_text_secondary" + android:drawablePadding="4dp" android:textSize="15sp" android:maxLines="1" app:layout_constraintEnd_toStartOf="@+id/incomingCallRejectView" @@ -65,6 +67,7 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" + app:tint="@color/white" android:src="@drawable/ic_call_answer" /> Date: Tue, 22 Dec 2020 12:34:20 +0100 Subject: [PATCH 29/57] VoIP: clean code --- .../java/im/vector/app/core/services/CallService.kt | 1 - .../im/vector/app/core/ui/views/CurrentCallsView.kt | 4 +--- .../main/java/im/vector/app/core/utils/CountUpTimer.kt | 5 +---- .../app/features/call/SharedKnownCallsViewModel.kt | 4 ++-- .../im/vector/app/features/call/VectorCallActivity.kt | 8 +++++++- .../im/vector/app/features/call/webrtc/WebRtcCall.kt | 4 ++++ .../app/features/call/webrtc/WebRtcCallManager.kt | 10 +++++++++- .../features/home/room/detail/RoomDetailFragment.kt | 2 +- .../room/detail/timeline/factory/MessageItemFactory.kt | 3 --- .../detail/timeline/factory/RoomCreateItemFactory.kt | 1 - .../detail/timeline/factory/VerificationItemFactory.kt | 1 - .../room/detail/timeline/helper/RoomSummaryHolder.kt | 3 +-- .../im/vector/app/features/navigation/Navigator.kt | 1 - .../app/features/notifications/NotificationUtils.kt | 2 -- 14 files changed, 26 insertions(+), 23 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index 0350971087..7571b6a205 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -309,7 +309,6 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe ContextCompat.startForegroundService(context, intent) } - fun onOutgoingCallRinging(context: Context, callId: String) { val intent = Intent(context, CallService::class.java) diff --git a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt index dfcd4629c6..4ad773f5f4 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt @@ -18,10 +18,8 @@ package im.vector.app.core.ui.views import android.content.Context import android.util.AttributeSet -import android.view.LayoutInflater import android.widget.RelativeLayout import im.vector.app.R -import im.vector.app.databinding.ViewCallControlsBinding import im.vector.app.databinding.ViewCurrentCallsBinding import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.themes.ThemeUtils @@ -62,7 +60,7 @@ class CurrentCallsView @JvmOverloads constructor( } } else { if (heldCalls.size > 1) { - views.currentCallsInfo.text = resources.getString(R.string.call_only_multiple_paused , heldCalls.size) + views.currentCallsInfo.text = resources.getString(R.string.call_only_multiple_paused, heldCalls.size) } else if (heldCalls.size == 1) { views.currentCallsInfo.text = resources.getString(R.string.call_active_and_single_paused, formattedDuration) } else { diff --git a/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt b/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt index 5b5a406194..2a9482765c 100644 --- a/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt +++ b/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt @@ -16,9 +16,7 @@ package im.vector.app.core.utils -import io.reactivex.Flowable import io.reactivex.Observable -import io.reactivex.disposables.Disposable import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicLong @@ -37,7 +35,7 @@ class CountUpTimer(private val intervalInMs: Long) { var tickListener: TickListener? = null - fun elapsedTime(): Long{ + fun elapsedTime(): Long { return elapsedTime.get() } @@ -56,5 +54,4 @@ class CountUpTimer(private val intervalInMs: Long) { interface TickListener { fun onTick(milliseconds: Long) } - } diff --git a/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt b/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt index 685b23f332..b33edd09e0 100644 --- a/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt @@ -32,13 +32,13 @@ class SharedKnownCallsViewModel @Inject constructor( val callListener = object : WebRtcCall.Listener { override fun onStateUpdate(call: MxCall) { - //post it-self + // post it-self liveKnownCalls.postValue(liveKnownCalls.value) } override fun onHoldUnhold() { super.onHoldUnhold() - //post it-self + // post it-self liveKnownCalls.postValue(liveKnownCalls.value) } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 9de0dbb7ed..f7fac4099c 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -251,7 +251,13 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } else { val otherCall = callManager.getCallById(state.otherKnownCallInfo.callId) val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) - avatarRenderer.renderBlur(state.otherKnownCallInfo.otherUserItem, views.otherKnownCallAvatarView, sampling = 20, rounded = false, colorFilter = colorFilter) + avatarRenderer.renderBlur( + matrixItem = state.otherKnownCallInfo.otherUserItem, + imageView = views.otherKnownCallAvatarView, + sampling = 20, + rounded = false, + colorFilter = colorFilter + ) views.otherKnownCallLayout.isVisible = true views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.remoteOnHold }.orFalse() } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 68cbc67b67..7ad99c2d50 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -558,11 +558,15 @@ class WebRtcCall(val mxCall: MxCall, remoteOnHold = true isLocalOnHold = true direction = RtpTransceiver.RtpTransceiverDirection.INACTIVE + timer.pause() } else { remoteOnHold = false isLocalOnHold = wasLocalOnHold onCallBecomeActive(this@WebRtcCall) direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV + if (!isLocalOnHold) { + timer.resume() + } } for (transceiver in peerConnection?.transceivers ?: emptyList()) { transceiver.direction = direction diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 883b966701..454e82db1e 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -206,6 +206,10 @@ class WebRtcCallManager @Inject constructor( fun startOutgoingCall(signalingRoomId: String, otherUserId: String, isVideoCall: Boolean) { Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") + if (getCallsByRoomId(signalingRoomId).isNotEmpty()) { + Timber.w("## VOIP you already have a call in this room") + return + } if (currentCall.get() != null && currentCall.get()?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) { Timber.w("## VOIP cannot start outgoing call") // Just ignore, maybe we could answer from other session? @@ -276,7 +280,11 @@ class WebRtcCallManager @Inject constructor( override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) { Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}") - if (currentCall.get() != null && currentCall.get()?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) { + if (getCallsByRoomId(mxCall.roomId).isNotEmpty()) { + Timber.w("## VOIP you already have a call in this room") + return + } + if ((currentCall.get() != null && currentCall.get()?.mxCall?.state !is CallState.Connected) || getCalls().size >= 2) { Timber.w("## VOIP receiving incoming call but cannot handle it") // Just ignore, maybe we could answer from other session? return diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 744595ecf0..ffd50f180b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -806,7 +806,7 @@ class RoomDetailFragment @Inject constructor( // resume existing if same room, if not prompt to kill and then restart new call? if (currentCall.mxCall.roomId == roomDetailArgs.roomId) { onTapToReturnToCall() - }else { + } else { safeStartCall(isVideoCall) } } else if (!state.isAllowedToStartWebRTCCall) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index d6473205a6..2c6176e87e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -16,8 +16,6 @@ package im.vector.app.features.home.room.detail.timeline.factory -import android.content.Intent -import android.os.Parcelable import android.text.SpannableStringBuilder import android.text.Spanned import android.text.TextPaint @@ -40,7 +38,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadSt import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt index 4c2029e347..31adbdb8a6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt @@ -21,7 +21,6 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder import im.vector.app.features.home.room.detail.timeline.item.RoomCreateItem_ import me.gujun.android.span.span import org.matrix.android.sdk.api.session.Session diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt index aadc1c1c38..0b623d78f1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt @@ -24,7 +24,6 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem_ import org.matrix.android.sdk.api.session.Session diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummaryHolder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummaryHolder.kt index ce1a7f3c3d..d3096afa9e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummaryHolder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummaryHolder.kt @@ -16,7 +16,6 @@ package im.vector.app.features.home.room.detail.timeline.helper -import im.vector.app.core.di.ScreenScope import org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject import javax.inject.Singleton @@ -36,7 +35,7 @@ class RoomSummaryHolder @Inject constructor() { fun get(roomId: String) = roomSummaries[roomId] - fun remove(roomId: String)= roomSummaries.remove(roomId) + fun remove(roomId: String) = roomSummaries.remove(roomId) fun clear() { roomSummaries.clear() diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 21a8d42848..504fccb63a 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -113,5 +113,4 @@ interface Navigator { options: ((MutableList>) -> Unit)?) fun openSearch(context: Context, roomId: String) - } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 48f0c54e76..d1e52b2a78 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -297,7 +297,6 @@ class NotificationUtils @Inject constructor(private val context: Context, .setLights(accentColor, 500, 500) .setOngoing(true) - val contentIntent = VectorCallActivity.newIntent( context = context, mxCall = mxCall, @@ -391,7 +390,6 @@ class NotificationUtils @Inject constructor(private val context: Context, @SuppressLint("NewApi") fun buildPendingCallNotification(mxCall: MxCall, title: String): Notification { - val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) .setContentTitle(ensureTitleNotEmpty(title)) .apply { From 9c5fe81792acd85673cbaa069bdbb49a1bf22a60 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 17 Dec 2020 14:33:22 +0100 Subject: [PATCH 30/57] VoIP: start to handle call transfer in SDK --- .../android/sdk/api/MatrixConfiguration.kt | 6 +- .../android/sdk/api/session/call/MxCall.kt | 3 + .../sdk/api/session/events/model/EventType.kt | 1 + .../room/model/call/CallAnswerContent.kt | 6 +- .../room/model/call/CallCapabilities.kt | 32 ++++++++ .../room/model/call/CallInviteContent.kt | 6 +- .../room/model/call/CallReplacesContent.kt | 81 +++++++++++++++++++ .../session/call/CallSignalingHandler.kt | 2 + .../internal/session/call/MxCallFactory.kt | 10 ++- .../internal/session/call/model/MxCallImpl.kt | 22 ++++- 10 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCapabilities.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index 725fd08d3b..5aca07e8cc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -35,7 +35,11 @@ data class MatrixConfiguration( * Optional proxy to connect to the matrix servers * You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port) */ - val proxy: Proxy? = null + val proxy: Proxy? = null, + /** + * True to advertise support for call transfers to other parties on Matrix calls. + */ + val supportsCallTransfer: Boolean = true ) { /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt index 75cff0e709..97bf724107 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.call import org.matrix.android.sdk.api.session.room.model.call.CallCandidate +import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.SdpType import org.matrix.android.sdk.api.util.Optional @@ -42,6 +43,8 @@ interface MxCall : MxCallDetail { var opponentPartyId: Optional? var opponentVersion: Int + var capabilities: CallCapabilities? + var state: CallState /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 85ba3100b0..3a4b4f82c3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/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_REPLACES = "m.call.replaces" // Key share events const val ROOM_KEY_REQUEST = "m.room_key_request" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt index 1fe4c3576f..e37595a129 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt @@ -39,7 +39,11 @@ data class CallAnswerContent( /** * Required. The version of the VoIP specification this messages adheres to. */ - @Json(name = "version") override val version: String? + @Json(name = "version") override val version: String?, + /** + * Capability advertisement. + */ + @Json(name= "capabilities") val capabilities: CallCapabilities? = null ): CallSignallingContent { @JsonClass(generateAdapter = true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCapabilities.kt new file mode 100644 index 0000000000..eb7edbeed3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCapabilities.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 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 org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.orFalse + +@JsonClass(generateAdapter = true) +data class CallCapabilities( + /** + * If set to true, states that the sender of the event supports the m.call.replaces event and therefore supports + * being transferred to another destination + */ + @Json(name = "m.call.transferee") val transferee: Boolean? = null +) + +fun CallCapabilities?.supportCallTransfer() = this?.transferee.orFalse() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt index dfbc5c64be..c89549184a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt @@ -49,7 +49,11 @@ data class CallInviteContent( /** * The field should be added for all invites where the target is a specific user */ - @Json(name = "invitee") val invitee: String? = null + @Json(name = "invitee") val invitee: String? = null, + /** + * Capability advertisement. + */ + @Json(name= "capabilities") val capabilities: CallCapabilities? = null ): CallSignallingContent { @JsonClass(generateAdapter = true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt new file mode 100644 index 0000000000..4d08a7c925 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This event is sent to signal the intent of a participant in a call to replace the call with another, + * such that the other participant ends up in a call with a new user. + */ +@JsonClass(generateAdapter = true) +data class CallReplacesContent( + /** + * Required. The ID of the call this event relates to. + */ + @Json(name = "call_id") override val callId: String, + /** + * Required. ID to let user identify remote echo of their own events + */ + @Json(name = "party_id") override val partyId: String? = null, + /** + * An identifier for the call replacement itself, generated by the transferor. + */ + @Json(name = "replacement_id") val replacementId: String? = null, + /** + * Optional. If specified, the transferee client waits for an invite to this room and joins it (possibly waiting for user confirmation) and then continues the transfer in this room. + * If absent, the transferee contacts the Matrix User ID given in the target_user field in a room of its choosing. + */ + @Json(name = "target_room") val targerRoomId: String? = null, + /** + * An object giving information about the transfer target + */ + @Json(name = "target_user") val targetUser: TargetUser? = null, + /** + * If specified, gives the call ID for the transferee's client to use when placing the replacement call. + * Mutually exclusive with await_call + */ + @Json(name = "create_call") val createCall: String? = null, + /** + * If specified, gives the call ID that the transferee's client should wait for. + * Mutually exclusive with create_call. + */ + @Json(name = "await_call") val awaitCall: String? = null, + /** + * Required. The version of the VoIP specification this messages adheres to. + */ + @Json(name = "version") override val version: String? +): CallSignallingContent { + + @JsonClass(generateAdapter = true) + data class TargetUser( + /** + * Required. The matrix user ID of the transfer target + */ + @Json(name = "id") val id: String, + /** + * Optional. The display name of the transfer target. + */ + @Json(name = "display_name") val displayName: String, + /** + * Optional. The avatar URL of the transfer target. + */ + @Json(name = "avatar_url") val avatarUrl: String + + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index a6de65f9f5..4576675d4a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent +import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent @@ -185,6 +186,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa call.apply { opponentPartyId = Optional.from(content.partyId) opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION + capabilities = content.capabilities ?: CallCapabilities() } callListenersDispatcher.onCallAnswerReceived(content) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt index 3c258df31a..e82d89fec6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt @@ -16,7 +16,9 @@ package org.matrix.android.sdk.internal.session.call +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.di.DeviceId @@ -32,6 +34,7 @@ internal class MxCallFactory @Inject constructor( @DeviceId private val deviceId: String?, private val localEchoEventFactory: LocalEchoEventFactory, private val eventSenderProcessor: EventSenderProcessor, + private val matrixConfiguration: MatrixConfiguration, @UserId private val userId: String ) { @@ -46,10 +49,12 @@ internal class MxCallFactory @Inject constructor( opponentUserId = opponentUserId, isVideoCall = content.isVideo(), localEchoEventFactory = localEchoEventFactory, - eventSenderProcessor = eventSenderProcessor + eventSenderProcessor = eventSenderProcessor, + matrixConfiguration = matrixConfiguration ).apply { opponentPartyId = Optional.from(content.partyId) opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION + capabilities = content.capabilities ?: CallCapabilities() } } @@ -63,7 +68,8 @@ internal class MxCallFactory @Inject constructor( opponentUserId = opponentUserId, isVideoCall = isVideoCall, localEchoEventFactory = localEchoEventFactory, - eventSenderProcessor = eventSenderProcessor + eventSenderProcessor = eventSenderProcessor, + matrixConfiguration = matrixConfiguration ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt index 9368e94efc..c1375bae6a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.call.model +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.events.model.Content @@ -27,6 +28,7 @@ import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidate import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent +import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent @@ -35,8 +37,8 @@ import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerConten import org.matrix.android.sdk.api.session.room.model.call.SdpType import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService -import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import timber.log.Timber internal class MxCallImpl( @@ -48,11 +50,13 @@ internal class MxCallImpl( override val isVideoCall: Boolean, override val ourPartyId: String, private val localEchoEventFactory: LocalEchoEventFactory, - private val eventSenderProcessor: EventSenderProcessor + private val eventSenderProcessor: EventSenderProcessor, + private val matrixConfiguration: MatrixConfiguration ) : MxCall { override var opponentPartyId: Optional? = null override var opponentVersion: Int = MxCall.VOIP_PROTO_VERSION + override var capabilities: CallCapabilities? = null override var state: CallState = CallState.Idle set(value) { @@ -98,7 +102,8 @@ internal class MxCallImpl( partyId = ourPartyId, lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS, offer = CallInviteContent.Offer(sdp = sdpString), - version = MxCall.VOIP_PROTO_VERSION.toString() + version = MxCall.VOIP_PROTO_VERSION.toString(), + capabilities = buildCapabilities() ) .let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } @@ -158,7 +163,8 @@ internal class MxCallImpl( callId = callId, partyId = ourPartyId, answer = CallAnswerContent.Answer(sdp = sdpString), - version = MxCall.VOIP_PROTO_VERSION.toString() + version = MxCall.VOIP_PROTO_VERSION.toString(), + capabilities = buildCapabilities() ) .let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } @@ -203,4 +209,12 @@ internal class MxCallImpl( ) .also { localEchoEventFactory.createLocalEcho(it) } } + + private fun buildCapabilities(): CallCapabilities? { + return if (matrixConfiguration.supportsCallTransfer) { + CallCapabilities(true) + } else { + null + } + } } From 8797d7562dc3b11dad41b1c4675c37f057e1275d Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 17 Dec 2020 17:05:39 +0100 Subject: [PATCH 31/57] VoIP: add call transfer method --- .../android/sdk/api/session/call/MxCall.kt | 5 +++ .../room/model/call/CallReplacesContent.kt | 4 +-- .../internal/session/call/MxCallFactory.kt | 8 +++-- .../internal/session/call/model/MxCallImpl.kt | 32 ++++++++++++++++++- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt index 97bf724107..2b7c0a7578 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt @@ -89,6 +89,11 @@ interface MxCall : MxCallDetail { */ fun sendLocalIceCandidateRemovals(candidates: List) + /** + * Send a m.call.replaces event to initiate call transfer. + */ + suspend fun transfer(targetUserId: String, targetRoomId: String?) + fun addListener(listener: StateListener) fun removeListener(listener: StateListener) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt index 4d08a7c925..310b46d03f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt @@ -71,11 +71,11 @@ data class CallReplacesContent( /** * Optional. The display name of the transfer target. */ - @Json(name = "display_name") val displayName: String, + @Json(name = "display_name") val displayName: String?, /** * Optional. The avatar URL of the transfer target. */ - @Json(name = "avatar_url") val avatarUrl: String + @Json(name = "avatar_url") val avatarUrl: String? ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt index e82d89fec6..4e60de6fd3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.call.model.MxCallImpl +import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import java.math.BigDecimal @@ -35,6 +36,7 @@ internal class MxCallFactory @Inject constructor( private val localEchoEventFactory: LocalEchoEventFactory, private val eventSenderProcessor: EventSenderProcessor, private val matrixConfiguration: MatrixConfiguration, + private val getProfileInfoTask: GetProfileInfoTask, @UserId private val userId: String ) { @@ -50,7 +52,8 @@ internal class MxCallFactory @Inject constructor( isVideoCall = content.isVideo(), localEchoEventFactory = localEchoEventFactory, eventSenderProcessor = eventSenderProcessor, - matrixConfiguration = matrixConfiguration + matrixConfiguration = matrixConfiguration, + getProfileInfoTask = getProfileInfoTask ).apply { opponentPartyId = Optional.from(content.partyId) opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION @@ -69,7 +72,8 @@ internal class MxCallFactory @Inject constructor( isVideoCall = isVideoCall, localEchoEventFactory = localEchoEventFactory, eventSenderProcessor = eventSenderProcessor, - matrixConfiguration = matrixConfiguration + matrixConfiguration = matrixConfiguration, + getProfileInfoTask = getProfileInfoTask ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt index c1375bae6a..ab7b74c229 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.UnsignedData import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidate import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent @@ -33,13 +34,16 @@ import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent +import org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent import org.matrix.android.sdk.api.session.room.model.call.SdpType import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService +import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import timber.log.Timber +import java.util.UUID internal class MxCallImpl( override val callId: String, @@ -51,7 +55,8 @@ internal class MxCallImpl( override val ourPartyId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val eventSenderProcessor: EventSenderProcessor, - private val matrixConfiguration: MatrixConfiguration + private val matrixConfiguration: MatrixConfiguration, + private val getProfileInfoTask: GetProfileInfoTask ) : MxCall { override var opponentPartyId: Optional? = null @@ -197,6 +202,31 @@ internal class MxCallImpl( .also { eventSenderProcessor.postEvent(it) } } + override suspend fun transfer(targetUserId: String, targetRoomId: String?) { + val profileInfoParams = GetProfileInfoTask.Params(targetUserId) + val profileInfo = try { + getProfileInfoTask.execute(profileInfoParams) + } catch (failure: Throwable) { + Timber.v("Fail fetching profile info of $targetUserId while transferring call") + null + } + CallReplacesContent( + callId = callId, + partyId = ourPartyId, + replacementId = UUID.randomUUID().toString(), + version = MxCall.VOIP_PROTO_VERSION.toString(), + targetUser = CallReplacesContent.TargetUser( + id = targetUserId, + displayName = profileInfo?.get(ProfileService.DISPLAY_NAME_KEY) as? String, + avatarUrl = profileInfo?.get(ProfileService.AVATAR_URL_KEY) as? String + ), + targerRoomId = targetRoomId, + createCall = UUID.randomUUID().toString() + ) + .let { createEventAndLocalEcho(type = EventType.CALL_REPLACES, roomId = roomId, content = it.toContent()) } + .also { eventSenderProcessor.postEvent(it) } + } + private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { return Event( roomId = roomId, From 22c981d8bf100089555318cdec1b822cfecdd978 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 22 Dec 2020 16:16:46 +0100 Subject: [PATCH 32/57] VoIP: start adding UI for call transfer --- vector/src/main/AndroidManifest.xml | 1 + .../im/vector/app/core/di/ScreenComponent.kt | 2 + .../call/transfer/CallTransferActivity.kt | 136 ++++++++++++++++++ .../call/transfer/CallTransferViewModel.kt | 50 +++++++ .../call/transfer/CallTransferViewState.kt | 26 ++++ .../contactsbook/ContactsBookFragment.kt | 8 +- .../contactsbook/ContactsBookViewModel.kt | 15 +- .../createdirect/CreateDirectRoomActivity.kt | 11 +- .../createdirect/CreateDirectRoomViewModel.kt | 9 +- .../invite/InviteUsersToRoomActivity.kt | 11 +- .../invite/InviteUsersToRoomViewModel.kt | 9 +- .../features/navigation/DefaultNavigator.kt | 7 + .../app/features/navigation/Navigator.kt | 2 + 13 files changed, 263 insertions(+), 24 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewState.kt diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 5035811b24..d1289563e9 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -243,6 +243,7 @@ + diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index f56a6a3d70..979c35dde7 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -28,6 +28,7 @@ import im.vector.app.features.MainActivity import im.vector.app.features.call.CallControlsBottomSheet import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.conference.VectorJitsiActivity +import im.vector.app.features.call.transfer.CallTransferActivity import im.vector.app.features.createdirect.CreateDirectRoomActivity import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.app.features.crypto.quads.SharedSecureStorageActivity @@ -146,6 +147,7 @@ interface ScreenComponent { fun inject(activity: VectorJitsiActivity) fun inject(activity: SearchActivity) fun inject(activity: UserCodeActivity) + fun inject(callTransferActivity: CallTransferActivity) /* ========================================================================================== * BottomSheets diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt new file mode 100644 index 0000000000..d038a0e836 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2020 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 im.vector.app.features.call.transfer + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import android.widget.Toast +import com.airbnb.mvrx.MvRx +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.extensions.addFragmentToBackstack +import im.vector.app.core.platform.SimpleFragmentActivity +import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH +import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS +import im.vector.app.core.utils.allGranted +import im.vector.app.core.utils.checkPermissions +import im.vector.app.features.contactsbook.ContactsBookFragment +import im.vector.app.features.contactsbook.ContactsBookViewModel +import im.vector.app.features.contactsbook.ContactsBookViewState +import im.vector.app.features.userdirectory.UserListFragment +import im.vector.app.features.userdirectory.UserListFragmentArgs +import im.vector.app.features.userdirectory.UserListSharedAction +import im.vector.app.features.userdirectory.UserListSharedActionViewModel +import im.vector.app.features.userdirectory.UserListViewModel +import im.vector.app.features.userdirectory.UserListViewState +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +@Parcelize +data class CallTransferArgs(val callId: String) : Parcelable + +class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Factory, UserListViewModel.Factory, ContactsBookViewModel.Factory { + + private lateinit var sharedActionViewModel: UserListSharedActionViewModel + @Inject lateinit var userListViewModelFactory: UserListViewModel.Factory + @Inject lateinit var callTransferViewModelFactory: CallTransferViewModel.Factory + @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory + @Inject lateinit var errorFormatter: ErrorFormatter + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + override fun create(initialState: UserListViewState): UserListViewModel { + return userListViewModelFactory.create(initialState) + } + + override fun create(initialState: CallTransferViewState): CallTransferViewModel { + return callTransferViewModelFactory.create(initialState) + } + + override fun create(initialState: ContactsBookViewState): ContactsBookViewModel { + return contactsBookViewModelFactory.create(initialState) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + views.toolbar.visibility = View.GONE + + sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java) + sharedActionViewModel + .observe() + .subscribe { sharedAction -> + when (sharedAction) { + UserListSharedAction.Close -> finish() + UserListSharedAction.GoBack -> onBackPressed() + UserListSharedAction.OpenPhoneBook -> openPhoneBook() + // not exhaustive because it's a sharedAction + else -> { + } + } + } + .disposeOnDestroy() + if (isFirstCreation()) { + addFragment( + R.id.container, + UserListFragment::class.java, + UserListFragmentArgs( + title = "Call transfer", + menuResId = -1 + ) + ) + } + } + + private fun openPhoneBook() { + // Check permission first + if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH, + this, + PERMISSION_REQUEST_CODE_READ_CONTACTS, + 0)) { + addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (allGranted(grantResults)) { + if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) { + doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) } + } + } else { + Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show() + } + } + + companion object { + + fun newIntent(context: Context, callId: String): Intent { + return Intent(context, CallTransferActivity::class.java).also { + it.putExtra(MvRx.KEY_ARG, CallTransferArgs(callId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt new file mode 100644 index 0000000000..fd9557315c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 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 im.vector.app.features.call.transfer + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.core.platform.EmptyAction +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import org.matrix.android.sdk.api.session.Session +import timber.log.Timber + +class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: CallTransferViewState): CallTransferViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: CallTransferViewState): CallTransferViewModel? { + val activity: CallTransferActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.callTransferViewModelFactory.create(state) + } + } + + override fun handle(action: EmptyAction) {} + +} diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewState.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewState.kt new file mode 100644 index 0000000000..2b29d9f6f2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 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 im.vector.app.features.call.transfer + +import com.airbnb.mvrx.MvRxState + +data class CallTransferViewState( + val callId: String +) : MvRxState { + + constructor(args: CallTransferArgs) : this(callId = args.callId) +} diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt index 6aaa69fbc0..3e85702cf4 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt @@ -44,9 +44,9 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject class ContactsBookFragment @Inject constructor( - val contactsBookViewModelFactory: ContactsBookViewModel.Factory, + private val contactsBookViewModelFactory: ContactsBookViewModel.Factory, private val contactsBookController: ContactsBookController -) : VectorBaseFragment(), ContactsBookController.Callback { +) : VectorBaseFragment(), ContactsBookController.Callback, ContactsBookViewModel.Factory { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentContactsBookBinding { return FragmentContactsBookBinding.inflate(inflater, container, false) @@ -59,6 +59,10 @@ class ContactsBookFragment @Inject constructor( private lateinit var sharedActionViewModel: UserListSharedActionViewModel + override fun create(initialState: ContactsBookViewState): ContactsBookViewModel { + return contactsBookViewModelFactory.create(initialState) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java) diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt index 2c4c5d0596..9721425f56 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt @@ -33,6 +33,7 @@ import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.createdirect.CreateDirectRoomActivity import im.vector.app.features.invite.InviteUsersToRoomActivity +import im.vector.app.features.userdirectory.UserListViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback @@ -56,17 +57,11 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted companion object : MvRxViewModelFactory { override fun create(viewModelContext: ViewModelContext, state: ContactsBookViewState): ContactsBookViewModel? { - return when (viewModelContext) { - is FragmentViewModelContext -> (viewModelContext.fragment() as ContactsBookFragment).contactsBookViewModelFactory.create(state) - is ActivityViewModelContext -> { - when (viewModelContext.activity()) { - is CreateDirectRoomActivity -> viewModelContext.activity().contactsBookViewModelFactory.create(state) - is InviteUsersToRoomActivity -> viewModelContext.activity().contactsBookViewModelFactory.create(state) - else -> error("Wrong activity or fragment") - } - } - else -> error("Wrong activity or fragment") + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") } } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt index beb7931fd4..347424ee8a 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt @@ -45,6 +45,7 @@ import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.contactsbook.ContactsBookViewModel +import im.vector.app.features.contactsbook.ContactsBookViewState import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.userdirectory.UserListFragmentArgs import im.vector.app.features.userdirectory.UserListSharedAction @@ -57,7 +58,7 @@ import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import java.net.HttpURLConnection import javax.inject.Inject -class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory { +class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory, CreateDirectRoomViewModel.Factory, ContactsBookViewModel.Factory { private val viewModel: CreateDirectRoomViewModel by viewModel() private lateinit var sharedActionViewModel: UserListSharedActionViewModel @@ -71,9 +72,11 @@ class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fac injector.inject(this) } - override fun create(initialState: UserListViewState): UserListViewModel { - return userListViewModelFactory.create(initialState) - } + override fun create(initialState: UserListViewState) = userListViewModelFactory.create(initialState) + + override fun create(initialState: CreateDirectRoomViewState) = createDirectRoomViewModelFactory.create(initialState) + + override fun create(initialState: ContactsBookViewState) = contactsBookViewModelFactory.create(initialState) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt index d074c93587..fdf16f2664 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt @@ -18,6 +18,7 @@ package im.vector.app.features.createdirect import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext @@ -25,6 +26,7 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.contactsbook.ContactsBookViewModel import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault import im.vector.app.features.userdirectory.PendingInvitee @@ -50,8 +52,11 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted @JvmStatic override fun create(viewModelContext: ViewModelContext, state: CreateDirectRoomViewState): CreateDirectRoomViewModel? { - val activity: CreateDirectRoomActivity = (viewModelContext as ActivityViewModelContext).activity() - return activity.createDirectRoomViewModelFactory.create(state) + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") } } diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt index 9949255e32..f923bb7306 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt @@ -39,6 +39,7 @@ import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.toast import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.contactsbook.ContactsBookViewModel +import im.vector.app.features.contactsbook.ContactsBookViewState import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.userdirectory.UserListFragmentArgs import im.vector.app.features.userdirectory.UserListSharedAction @@ -53,7 +54,7 @@ import javax.inject.Inject @Parcelize data class InviteUsersToRoomArgs(val roomId: String) : Parcelable -class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory { +class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory, ContactsBookViewModel.Factory, InviteUsersToRoomViewModel.Factory { private val viewModel: InviteUsersToRoomViewModel by viewModel() private lateinit var sharedActionViewModel: UserListSharedActionViewModel @@ -67,9 +68,11 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa injector.inject(this) } - override fun create(initialState: UserListViewState): UserListViewModel { - return userListViewModelFactory.create(initialState) - } + override fun create(initialState: UserListViewState) = userListViewModelFactory.create(initialState) + + override fun create(initialState: ContactsBookViewState) = contactsBookViewModelFactory.create(initialState) + + override fun create(initialState: InviteUsersToRoomViewState) = inviteUsersToRoomViewModelFactory.create(initialState) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt index 21a998d8e2..984eed33b0 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt @@ -17,6 +17,7 @@ package im.vector.app.features.invite import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted @@ -24,6 +25,7 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.contactsbook.ContactsBookViewModel import im.vector.app.features.userdirectory.PendingInvitee import io.reactivex.Observable import org.matrix.android.sdk.api.session.Session @@ -46,8 +48,11 @@ class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted @JvmStatic override fun create(viewModelContext: ViewModelContext, state: InviteUsersToRoomViewState): InviteUsersToRoomViewModel? { - val activity: InviteUsersToRoomActivity = (viewModelContext as ActivityViewModelContext).activity() - return activity.inviteUsersToRoomViewModelFactory.create(state) + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 0a6197e424..5f785acbaf 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -34,6 +34,8 @@ import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.conference.VectorJitsiActivity +import im.vector.app.features.call.transfer.CallTransferActivity +import im.vector.app.features.call.transfer.CallTransferArgs import im.vector.app.features.createdirect.CreateDirectRoomActivity import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity @@ -344,6 +346,11 @@ class DefaultNavigator @Inject constructor( context.startActivity(intent) } + override fun openCallTransfer(context: Context, callId: String) { + val intent = CallTransferActivity.newIntent(context, callId) + context.startActivity(intent) + } + private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) { if (buildTask) { val stackBuilder = TaskStackBuilder.create(context) diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 504fccb63a..bc13e99bbf 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -113,4 +113,6 @@ interface Navigator { options: ((MutableList>) -> Unit)?) fun openSearch(context: Context, roomId: String) + + fun openCallTransfer(context: Context, callId: String) } From 439ea42b54458f9a764a8b31b725e78e9727bae1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 22 Dec 2020 16:32:32 +0100 Subject: [PATCH 33/57] VoIP: use UserListFragment to select someone for call transfer (+ clean some code) --- .../java/im/vector/app/core/extensions/Set.kt | 14 +++- .../app/core/platform/VectorBaseActivity.kt | 15 ++++ .../features/call/CallControlsBottomSheet.kt | 6 ++ .../app/features/call/VectorCallActivity.kt | 9 ++- .../features/call/VectorCallViewActions.kt | 1 + .../app/features/call/VectorCallViewEvents.kt | 1 + .../app/features/call/VectorCallViewModel.kt | 12 ++- .../app/features/call/VectorCallViewState.kt | 3 +- .../call/transfer/CallTransferAction.kt | 23 ++++++ .../call/transfer/CallTransferActivity.kt | 58 ++++++++++----- .../call/transfer/CallTransferViewEvents.kt | 23 ++++++ .../call/transfer/CallTransferViewModel.kt | 54 ++++++++++++-- .../contactsbook/ContactsBookFragment.kt | 6 +- .../createdirect/CreateDirectRoomAction.kt | 4 +- .../createdirect/CreateDirectRoomActivity.kt | 2 +- .../CreateDirectRoomByQrCodeFragment.kt | 5 +- .../createdirect/CreateDirectRoomViewModel.kt | 13 ++-- .../invite/InviteUsersToRoomAction.kt | 4 +- .../invite/InviteUsersToRoomActivity.kt | 5 +- .../invite/InviteUsersToRoomViewModel.kt | 27 ++++--- ...{PendingInvitee.kt => PendingSelection.kt} | 10 +-- .../features/userdirectory/UserListAction.kt | 4 +- .../userdirectory/UserListController.kt | 9 +-- .../userdirectory/UserListFragment.kt | 38 ++++++---- .../userdirectory/UserListFragmentArgs.kt | 4 +- .../userdirectory/UserListSharedAction.kt | 2 +- .../userdirectory/UserListViewModel.kt | 26 +++---- .../userdirectory/UserListViewState.kt | 18 +++-- .../main/res/drawable/ic_call_transfer.xml | 17 +++++ .../res/layout/activity_call_transfer.xml | 73 +++++++++++++++++++ .../res/layout/bottom_sheet_call_controls.xml | 9 +++ vector/src/main/res/values/strings.xml | 4 + 32 files changed, 376 insertions(+), 123 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt rename vector/src/main/java/im/vector/app/features/userdirectory/{PendingInvitee.kt => PendingSelection.kt} (73%) create mode 100644 vector/src/main/res/drawable/ic_call_transfer.xml create mode 100644 vector/src/main/res/layout/activity_call_transfer.xml diff --git a/vector/src/main/java/im/vector/app/core/extensions/Set.kt b/vector/src/main/java/im/vector/app/core/extensions/Set.kt index a78fb85a1d..e1787076b9 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Set.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Set.kt @@ -17,10 +17,18 @@ package im.vector.app.core.extensions // Create a new Set including the provided element if not already present, or removing the element if already present -fun Set.toggle(element: T): Set { +fun Set.toggle(element: T, singleElement: Boolean = false): Set { return if (contains(element)) { - minus(element) + if (singleElement) { + emptySet() + } else { + minus(element) + } } else { - plus(element) + if (singleElement) { + setOf(element) + } else { + plus(element) + } } } diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index a585e8ea77..268ec7eb26 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -42,6 +42,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.viewbinding.ViewBinding import com.bumptech.glide.util.Util import com.google.android.material.snackbar.Snackbar +import com.jakewharton.rxbinding3.view.clicks import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder @@ -84,6 +85,7 @@ import io.reactivex.disposables.Disposable import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.GlobalError import timber.log.Timber +import java.util.concurrent.TimeUnit import kotlin.system.measureTimeMillis abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { @@ -113,6 +115,19 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScr .disposeOnDestroy() } + /* ========================================================================================== + * Views + * ========================================================================================== */ + + protected fun View.debouncedClicks(onClicked: () -> Unit) { + clicks() + .throttleFirst(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { onClicked() } + .disposeOnDestroy() + } + + /* ========================================================================================== * DATA * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt index a94a030e34..eab788f461 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt @@ -63,6 +63,11 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment { @@ -153,5 +158,6 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment(), CallContro views.callConnectingProgress.isVisible = true configureCallInfo(state) } - is CallState.Connected -> { + is CallState.Connected -> { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (state.isLocalOnHold || state.isRemoteOnHold) { views.smallIsHeldIcon.isVisible = true @@ -227,10 +227,10 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callConnectingProgress.isVisible = true } } - is CallState.Terminated -> { + is CallState.Terminated -> { finish() } - null -> { + null -> { } } } @@ -320,6 +320,9 @@ class VectorCallActivity : VectorBaseActivity(), CallContro is VectorCallViewEvents.ConnectionTimeout -> { onErrorTimoutConnect(event.turn) } + is VectorCallViewEvents.ShowCallTransferScreen -> { + navigator.openCallTransfer(this, callArgs.callId) + } null -> { } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt index 83ac878186..adb8897a51 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt @@ -30,4 +30,5 @@ sealed class VectorCallViewActions : VectorViewModelAction { object HeadSetButtonPressed : VectorCallViewActions() object ToggleCamera : VectorCallViewActions() object ToggleHDSD : VectorCallViewActions() + object InitiateCallTransfer : VectorCallViewActions() } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt index b79cd5d772..832c4fe944 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt @@ -27,6 +27,7 @@ sealed class VectorCallViewEvents : VectorViewEvents { val available: List, val current: CallAudioManager.SoundDevice ) : VectorCallViewEvents() + object ShowCallTransferScreen: VectorCallViewEvents() // data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents() // data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents() // object CallAccepted : VectorCallViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 6f5a9213ae..0c621dcfe4 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.call.TurnServerResponse +import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem import java.util.Timer @@ -114,7 +115,8 @@ class VectorCallViewModel @AssistedInject constructor( } setState { copy( - callState = Success(callState) + callState = Success(callState), + canOpponentBeTransferred = call.capabilities.supportCallTransfer() ) } } @@ -188,7 +190,8 @@ class VectorCallViewModel @AssistedInject constructor( isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT, canSwitchCamera = webRtcCall.canSwitchCamera(), formattedDuration = webRtcCall.formattedDuration(), - isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD + isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD, + canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer() ) } updateOtherKnownCall(webRtcCall) @@ -269,6 +272,11 @@ class VectorCallViewModel @AssistedInject constructor( if (!state.isVideoCall) return@withState call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD) } + VectorCallViewActions.InitiateCallTransfer -> { + _viewEvents.post( + VectorCallViewEvents.ShowCallTransferScreen + ) + } }.exhaustive } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt index 15fa2a37fa..1d12b780b1 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt @@ -39,7 +39,8 @@ data class VectorCallViewState( val callState: Async = Uninitialized, val otherKnownCallInfo: CallInfo? = null, val callInfo: CallInfo = CallInfo(callId), - val formattedDuration: String = "" + val formattedDuration: String = "", + val canOpponentBeTransferred: Boolean = false ) : MvRxState { data class CallInfo( diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt new file mode 100644 index 0000000000..cbdd9c252b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 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 im.vector.app.features.call.transfer + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class CallTransferAction : VectorViewModelAction { + data class Connect(val consultFirst: Boolean, val selectedUserId: String) : CallTransferAction() +} diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt index d038a0e836..ec517c813b 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt @@ -20,19 +20,20 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable -import android.view.View import android.widget.Toast import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.viewModel import im.vector.app.R import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack -import im.vector.app.core.platform.SimpleFragmentActivity +import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.checkPermissions +import im.vector.app.databinding.ActivityCallTransferBinding import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.contactsbook.ContactsBookViewModel import im.vector.app.features.contactsbook.ContactsBookViewState @@ -48,7 +49,9 @@ import javax.inject.Inject @Parcelize data class CallTransferArgs(val callId: String) : Parcelable -class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Factory, UserListViewModel.Factory, ContactsBookViewModel.Factory { +private const val USER_LIST_FRAGMENT_TAG = "USER_LIST_FRAGMENT_TAG" + +class CallTransferActivity : VectorBaseActivity(), CallTransferViewModel.Factory, UserListViewModel.Factory, ContactsBookViewModel.Factory { private lateinit var sharedActionViewModel: UserListSharedActionViewModel @Inject lateinit var userListViewModelFactory: UserListViewModel.Factory @@ -56,6 +59,10 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter + private val callTransferViewModel: CallTransferViewModel by viewModel() + + override fun getBinding() = ActivityCallTransferBinding.inflate(layoutInflater) + override fun injectWith(injector: ScreenComponent) { super.injectWith(injector) injector.inject(this) @@ -75,33 +82,50 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - views.toolbar.visibility = View.GONE - sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java) sharedActionViewModel .observe() .subscribe { sharedAction -> when (sharedAction) { - UserListSharedAction.Close -> finish() - UserListSharedAction.GoBack -> onBackPressed() - UserListSharedAction.OpenPhoneBook -> openPhoneBook() + UserListSharedAction.OpenPhoneBook -> openPhoneBook() // not exhaustive because it's a sharedAction - else -> { + else -> { } } } .disposeOnDestroy() if (isFirstCreation()) { addFragment( - R.id.container, + R.id.callTransferFragmentContainer, UserListFragment::class.java, UserListFragmentArgs( - title = "Call transfer", - menuResId = -1 - ) + title = "", + menuResId = -1, + singleSelection = true, + showInviteActions = false, + showToolbar = false + ), + USER_LIST_FRAGMENT_TAG ) } + callTransferViewModel.observeViewEvents { + when (it) { + is CallTransferViewEvents.Dismiss -> finish() + } + } + configureToolbar(views.callTransferToolbar) + setupConnectAction() + } + + private fun setupConnectAction() { + views.callTransferConnectAction.debouncedClicks { + val userListFragment = supportFragmentManager.findFragmentByTag(USER_LIST_FRAGMENT_TAG) as? UserListFragment + val selectedUser = userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull() + if (selectedUser != null) { + val action = CallTransferAction.Connect(views.callTransferConsultCheckBox.isChecked, selectedUser) + callTransferViewModel.handle(action) + } + } } private fun openPhoneBook() { @@ -110,7 +134,7 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac this, PERMISSION_REQUEST_CODE_READ_CONTACTS, 0)) { - addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) + addFragmentToBackstack(R.id.callTransferFragmentContainer, ContactsBookFragment::class.java) } } @@ -118,7 +142,7 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (allGranted(grantResults)) { if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) { - doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) } + doOnPostResume { addFragmentToBackstack(R.id.callTransferFragmentContainer, ContactsBookFragment::class.java) } } } else { Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show() @@ -126,7 +150,7 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac } companion object { - + fun newIntent(context: Context, callId: String): Intent { return Intent(context, CallTransferActivity::class.java).also { it.putExtra(MvRx.KEY_ARG, CallTransferArgs(callId)) diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt new file mode 100644 index 0000000000..b4a228efca --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 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 im.vector.app.features.call.transfer + +import im.vector.app.core.platform.VectorViewEvents + +sealed class CallTransferViewEvents : VectorViewEvents { + object Dismiss : CallTransferViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt index fd9557315c..78c7cc4664 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt @@ -16,20 +16,23 @@ package im.vector.app.features.call.transfer +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import im.vector.app.core.platform.EmptyAction -import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel -import im.vector.app.core.resources.StringProvider -import org.matrix.android.sdk.api.session.Session +import im.vector.app.features.call.webrtc.WebRtcCall +import im.vector.app.features.call.webrtc.WebRtcCallManager +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.call.CallState +import org.matrix.android.sdk.api.session.call.MxCall import timber.log.Timber -class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState) - : VectorViewModel(initialState) { +class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState, + private val callManager: WebRtcCallManager) + : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -45,6 +48,43 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: } } - override fun handle(action: EmptyAction) {} + private var call: WebRtcCall? = null + private val callListener = object : WebRtcCall.Listener { + override fun onStateUpdate(call: MxCall) { + if (call.state == CallState.Terminated) { + _viewEvents.post(CallTransferViewEvents.Dismiss) + } + } + } + init { + val webRtcCall = callManager.getCallById(initialState.callId) + if (webRtcCall == null) { + _viewEvents.post(CallTransferViewEvents.Dismiss) + } else { + call = webRtcCall + webRtcCall.addListener(callListener) + } + } + + override fun onCleared() { + super.onCleared() + call?.removeListener(callListener) + } + + override fun handle(action: CallTransferAction) { + when (action) { + is CallTransferAction.Connect -> transferCall(action) + } + } + + private fun transferCall(action: CallTransferAction.Connect) { + viewModelScope.launch { + try { + call?.mxCall?.transfer(action.selectedUserId, null) + } catch (failure: Throwable) { + Timber.v("Fail to transfer call: $failure") + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt index 3e85702cf4..68e169b8c5 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt @@ -32,7 +32,7 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentContactsBookBinding -import im.vector.app.features.userdirectory.PendingInvitee +import im.vector.app.features.userdirectory.PendingSelection import im.vector.app.features.userdirectory.UserListAction import im.vector.app.features.userdirectory.UserListSharedAction import im.vector.app.features.userdirectory.UserListSharedActionViewModel @@ -132,13 +132,13 @@ class ContactsBookFragment @Inject constructor( override fun onMatrixIdClick(matrixId: String) { view?.hideKeyboard() - viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId)))) + viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.UserPendingSelection(User(matrixId)))) sharedActionViewModel.post(UserListSharedAction.GoBack) } override fun onThreePidClick(threePid: ThreePid) { view?.hideKeyboard() - viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid))) + viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.ThreePidPendingSelection(threePid))) sharedActionViewModel.post(UserListSharedAction.GoBack) } } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt index ce91761fdd..ffc25210e9 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt @@ -17,11 +17,11 @@ package im.vector.app.features.createdirect import im.vector.app.core.platform.VectorViewModelAction -import im.vector.app.features.userdirectory.PendingInvitee +import im.vector.app.features.userdirectory.PendingSelection sealed class CreateDirectRoomAction : VectorViewModelAction { data class CreateRoomAndInviteSelectedUsers( - val invitees: Set, + val selections: Set, val existingDmRoomId: String? ) : CreateDirectRoomAction() } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt index 347424ee8a..4f81841b73 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt @@ -146,7 +146,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fac private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) { if (action.itemId == R.id.action_create_direct_room) { viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers( - action.invitees, + action.selections, null )) } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt index 94578ed5c7..92a03c5483 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt @@ -29,8 +29,7 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentQrCodeScannerBinding -import im.vector.app.features.userdirectory.PendingInvitee - +import im.vector.app.features.userdirectory.PendingSelection import me.dm7.barcodescanner.zxing.ZXingScannerView import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser @@ -107,7 +106,7 @@ class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragmen val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null) viewModel.handle( - CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingInvitee.UserPendingInvitee(qrInvitee)), existingDm) + CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingSelection.UserPendingSelection(qrInvitee)), existingDm) ) } } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt index fdf16f2664..9ac8ed2450 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt @@ -26,10 +26,9 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.contactsbook.ContactsBookViewModel import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault -import im.vector.app.features.userdirectory.PendingInvitee +import im.vector.app.features.userdirectory.PendingSelection import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.raw.RawService @@ -77,11 +76,11 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } } else { // Create the DM - createRoomAndInviteSelectedUsers(action.invitees) + createRoomAndInviteSelectedUsers(action.selections) } } - private fun createRoomAndInviteSelectedUsers(invitees: Set) { + private fun createRoomAndInviteSelectedUsers(selections: Set) { viewModelScope.launch(Dispatchers.IO) { val adminE2EByDefault = rawService.getElementWellknown(session.myUserId) ?.isE2EByDefault() @@ -89,10 +88,10 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted val roomParams = CreateRoomParams() .apply { - invitees.forEach { + selections.forEach { when (it) { - is PendingInvitee.UserPendingInvitee -> invitedUserIds.add(it.user.userId) - is PendingInvitee.ThreePidPendingInvitee -> invite3pids.add(it.threePid) + is PendingSelection.UserPendingSelection -> invitedUserIds.add(it.user.userId) + is PendingSelection.ThreePidPendingSelection -> invite3pids.add(it.threePid) }.exhaustive } setDirectMessage() diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomAction.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomAction.kt index cd9df8dc96..be9ad61868 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomAction.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomAction.kt @@ -17,8 +17,8 @@ package im.vector.app.features.invite import im.vector.app.core.platform.VectorViewModelAction -import im.vector.app.features.userdirectory.PendingInvitee +import im.vector.app.features.userdirectory.PendingSelection sealed class InviteUsersToRoomAction : VectorViewModelAction { - data class InviteSelectedUsers(val invitees: Set) : InviteUsersToRoomAction() + data class InviteSelectedUsers(val selections: Set) : InviteUsersToRoomAction() } diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt index f923bb7306..f9f5b2b995 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt @@ -95,7 +95,6 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa } .disposeOnDestroy() if (isFirstCreation()) { - val args: InviteUsersToRoomArgs? = intent.extras?.getParcelable(MvRx.KEY_ARG) addFragment( R.id.container, UserListFragment::class.java, @@ -103,7 +102,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa title = getString(R.string.invite_users_to_room_title), menuResId = R.menu.vector_invite_users_to_room, excludedUserIds = viewModel.getUserIdsOfRoomMembers(), - existingRoomId = args?.roomId + showInviteActions = false ) ) } @@ -113,7 +112,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) { if (action.itemId == R.id.action_invite_users_to_room_invite) { - viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees)) + viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.selections)) } } diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt index 984eed33b0..c0e9ea6d51 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt @@ -25,8 +25,7 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider -import im.vector.app.features.contactsbook.ContactsBookViewModel -import im.vector.app.features.userdirectory.PendingInvitee +import im.vector.app.features.userdirectory.PendingSelection import io.reactivex.Observable import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.rx.rx @@ -58,30 +57,30 @@ class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted override fun handle(action: InviteUsersToRoomAction) { when (action) { - is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.invitees) + is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.selections) } } - private fun inviteUsersToRoom(invitees: Set) { + private fun inviteUsersToRoom(selections: Set) { _viewEvents.post(InviteUsersToRoomViewEvents.Loading) - Observable.fromIterable(invitees).flatMapCompletable { user -> + Observable.fromIterable(selections).flatMapCompletable { user -> when (user) { - is PendingInvitee.UserPendingInvitee -> room.rx().invite(user.user.userId, null) - is PendingInvitee.ThreePidPendingInvitee -> room.rx().invite3pid(user.threePid) + is PendingSelection.UserPendingSelection -> room.rx().invite(user.user.userId, null) + is PendingSelection.ThreePidPendingSelection -> room.rx().invite3pid(user.threePid) } }.subscribe( { - val successMessage = when (invitees.size) { + val successMessage = when (selections.size) { 1 -> stringProvider.getString(R.string.invitation_sent_to_one_user, - invitees.first().getBestName()) + selections.first().getBestName()) 2 -> stringProvider.getString(R.string.invitations_sent_to_two_users, - invitees.first().getBestName(), - invitees.last().getBestName()) + selections.first().getBestName(), + selections.last().getBestName()) else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users, - invitees.size - 1, - invitees.first().getBestName(), - invitees.size - 1) + selections.size - 1, + selections.first().getBestName(), + selections.size - 1) } _viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage)) }, diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/PendingInvitee.kt b/vector/src/main/java/im/vector/app/features/userdirectory/PendingSelection.kt similarity index 73% rename from vector/src/main/java/im/vector/app/features/userdirectory/PendingInvitee.kt rename to vector/src/main/java/im/vector/app/features/userdirectory/PendingSelection.kt index f7213497fa..57f950b2c8 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/PendingInvitee.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/PendingSelection.kt @@ -19,14 +19,14 @@ package im.vector.app.features.userdirectory import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.user.model.User -sealed class PendingInvitee { - data class UserPendingInvitee(val user: User) : PendingInvitee() - data class ThreePidPendingInvitee(val threePid: ThreePid) : PendingInvitee() +sealed class PendingSelection { + data class UserPendingSelection(val user: User) : PendingSelection() + data class ThreePidPendingSelection(val threePid: ThreePid) : PendingSelection() fun getBestName(): String { return when (this) { - is UserPendingInvitee -> user.getBestName() - is ThreePidPendingInvitee -> threePid.value + is UserPendingSelection -> user.getBestName() + is ThreePidPendingSelection -> threePid.value } } } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt index 0c2c4b1f4b..7835232b09 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt @@ -21,7 +21,7 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class UserListAction : VectorViewModelAction { data class SearchUsers(val value: String) : UserListAction() object ClearSearchUsers : UserListAction() - data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction() - data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction() + data class AddPendingSelection(val pendingSelection: PendingSelection) : UserListAction() + data class RemovePendingSelection(val pendingSelection: PendingSelection) : UserListAction() object ComputeMatrixToLinkForSharing : UserListAction() } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt index 3e1523d0cc..331329ae61 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt @@ -54,10 +54,7 @@ class UserListController @Inject constructor(private val session: Session, // Build generic items if (currentState.searchTerm.isBlank()) { - // For now we remove this option if in invite to existing room flow (and not create DM) - if (currentState.pendingInvitees.isEmpty() - // For now we remove this option if in invite to existing room flow (and not create DM) - && currentState.existingRoomId == null) { + if (currentState.showInviteActions()) { actionItem { id(R.drawable.ic_share) title(stringProvider.getString(R.string.invite_friends)) @@ -75,9 +72,7 @@ class UserListController @Inject constructor(private val session: Session, callback?.onContactBookClick() }) } - if (currentState.pendingInvitees.isEmpty() - // For now we remove this option if in invite to existing room flow (and not create DM) - && currentState.existingRoomId == null) { + if (currentState.showInviteActions()) { actionItem { id(R.drawable.ic_qr_code_add) title(stringProvider.getString(R.string.qr_code)) diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt index d06030c301..0dca6802c4 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt @@ -67,18 +67,22 @@ class UserListFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java) - views.userListTitle.text = args.title - vectorBaseActivity.setSupportActionBar(views.userListToolbar) - + if(args.showToolbar) { + views.userListTitle.text = args.title + vectorBaseActivity.setSupportActionBar(views.userListToolbar) + setupCloseView() + views.userListToolbar.isVisible = true + }else{ + views.userListToolbar.isVisible = false + } setupRecyclerView() setupSearchView() - setupCloseView() homeServerCapabilitiesViewModel.subscribe { views.userListE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault } - viewModel.selectSubscribe(this, UserListViewState::pendingInvitees) { + viewModel.selectSubscribe(this, UserListViewState::pendingSelections) { renderSelectedUsers(it) } @@ -105,7 +109,7 @@ class UserListFragment @Inject constructor( override fun onPrepareOptionsMenu(menu: Menu) { withState(viewModel) { - val showMenuItem = it.pendingInvitees.isNotEmpty() + val showMenuItem = it.pendingSelections.isNotEmpty() menu.forEach { menuItem -> menuItem.isVisible = showMenuItem } @@ -114,7 +118,7 @@ class UserListFragment @Inject constructor( } override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) { - sharedActionViewModel.post(UserListSharedAction.OnMenuItemSelected(item.itemId, it.pendingInvitees)) + sharedActionViewModel.post(UserListSharedAction.OnMenuItemSelected(item.itemId, it.pendingSelections)) return@withState true } @@ -156,14 +160,14 @@ class UserListFragment @Inject constructor( userListController.setData(it) } - private fun renderSelectedUsers(invitees: Set) { + private fun renderSelectedUsers(selections: Set) { invalidateOptionsMenu() val currentNumberOfChips = views.chipGroup.childCount - val newNumberOfChips = invitees.size + val newNumberOfChips = selections.size views.chipGroup.removeAllViews() - invitees.forEach { addChipToGroup(it) } + selections.forEach { addChipToGroup(it) } // Scroll to the bottom when adding chips. When removing chips, do not scroll if (newNumberOfChips >= currentNumberOfChips) { @@ -173,20 +177,22 @@ class UserListFragment @Inject constructor( } } - private fun addChipToGroup(pendingInvitee: PendingInvitee) { + private fun addChipToGroup(pendingSelection: PendingSelection) { val chip = Chip(requireContext()) chip.setChipBackgroundColorResource(android.R.color.transparent) chip.chipStrokeWidth = dimensionConverter.dpToPx(1).toFloat() - chip.text = pendingInvitee.getBestName() + chip.text = pendingSelection.getBestName() chip.isClickable = true chip.isCheckable = false chip.isCloseIconVisible = true views.chipGroup.addView(chip) chip.setOnCloseIconClickListener { - viewModel.handle(UserListAction.RemovePendingInvitee(pendingInvitee)) + viewModel.handle(UserListAction.RemovePendingSelection(pendingSelection)) } } + fun getCurrentState() = withState(viewModel){ it } + override fun onInviteFriendClick() { viewModel.handle(UserListAction.ComputeMatrixToLinkForSharing) } @@ -197,17 +203,17 @@ class UserListFragment @Inject constructor( override fun onItemClick(user: User) { view?.hideKeyboard() - viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user))) + viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.UserPendingSelection(user))) } override fun onMatrixIdClick(matrixId: String) { view?.hideKeyboard() - viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId)))) + viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.UserPendingSelection(User(matrixId)))) } override fun onThreePidClick(threePid: ThreePid) { view?.hideKeyboard() - viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid))) + viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.ThreePidPendingSelection(threePid))) } override fun onUseQRCode() { diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt index 02fd13b39b..dce1f46b2f 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt @@ -24,5 +24,7 @@ data class UserListFragmentArgs( val title: String, val menuResId: Int, val excludedUserIds: Set? = null, - val existingRoomId: String? = null + val singleSelection: Boolean = false, + val showInviteActions: Boolean = true, + val showToolbar: Boolean = true ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt index b2cdee3e63..fca771793b 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt @@ -21,7 +21,7 @@ import im.vector.app.core.platform.VectorSharedAction sealed class UserListSharedAction : VectorSharedAction { object Close : UserListSharedAction() object GoBack : UserListSharedAction() - data class OnMenuItemSelected(val itemId: Int, val invitees: Set) : UserListSharedAction() + data class OnMenuItemSelected(val itemId: Int, val selections: Set) : UserListSharedAction() object OpenPhoneBook : UserListSharedAction() object AddByQrCode : UserListSharedAction() } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt index f8eabbaed0..322f5eef21 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt @@ -68,21 +68,15 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User } init { - setState { - copy( - myUserId = session.myUserId, - existingRoomId = initialState.existingRoomId - ) - } observeUsers() } override fun handle(action: UserListAction) { when (action) { - is UserListAction.SearchUsers -> handleSearchUsers(action.value) - is UserListAction.ClearSearchUsers -> handleClearSearchUsers() - is UserListAction.SelectPendingInvitee -> handleSelectUser(action) - is UserListAction.RemovePendingInvitee -> handleRemoveSelectedUser(action) + is UserListAction.SearchUsers -> handleSearchUsers(action.value) + is UserListAction.ClearSearchUsers -> handleClearSearchUsers() + is UserListAction.AddPendingSelection -> handleSelectUser(action) + is UserListAction.RemovePendingSelection -> handleRemoveSelectedUser(action) UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink() }.exhaustive } @@ -168,13 +162,13 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User .disposeOnClear() } - private fun handleSelectUser(action: UserListAction.SelectPendingInvitee) = withState { state -> - val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee) - setState { copy(pendingInvitees = selectedUsers) } + private fun handleSelectUser(action: UserListAction.AddPendingSelection) = withState { state -> + val selections = state.pendingSelections.toggle(action.pendingSelection, singleElement = state.singleSelection) + setState { copy(pendingSelections = selections) } } - private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingInvitee) = withState { state -> - val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee) - setState { copy(pendingInvitees = selectedUsers) } + private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingSelection) = withState { state -> + val selections = state.pendingSelections.minus(action.pendingSelection) + setState { copy(pendingSelections = selections) } } } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt index 69135f912d..1c7ccc70dd 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt @@ -28,24 +28,28 @@ data class UserListViewState( val knownUsers: Async> = Uninitialized, val directoryUsers: Async> = Uninitialized, val filteredMappedContacts: List = emptyList(), - val pendingInvitees: Set = emptySet(), - val createAndInviteState: Async = Uninitialized, + val pendingSelections: Set = emptySet(), val searchTerm: String = "", val myUserId: String = "", - val existingRoomId: String? = null + val singleSelection: Boolean, + private val showInviteActions: Boolean ) : MvRxState { constructor(args: UserListFragmentArgs) : this( - existingRoomId = args.existingRoomId + singleSelection = args.singleSelection, + showInviteActions = args.showInviteActions ) fun getSelectedMatrixId(): List { - return pendingInvitees + return pendingSelections .mapNotNull { when (it) { - is PendingInvitee.UserPendingInvitee -> it.user.userId - is PendingInvitee.ThreePidPendingInvitee -> null + is PendingSelection.UserPendingSelection -> it.user.userId + is PendingSelection.ThreePidPendingSelection -> null } } } + + fun showInviteActions() = showInviteActions && pendingSelections.isEmpty() + } diff --git a/vector/src/main/res/drawable/ic_call_transfer.xml b/vector/src/main/res/drawable/ic_call_transfer.xml new file mode 100644 index 0000000000..b2c28d6ba0 --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_transfer.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/vector/src/main/res/layout/activity_call_transfer.xml b/vector/src/main/res/layout/activity_call_transfer.xml new file mode 100644 index 0000000000..b748591788 --- /dev/null +++ b/vector/src/main/res/layout/activity_call_transfer.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + +