diff --git a/build.gradle b/build.gradle
index 0f94fc418c..7e5d659c8b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -29,7 +29,7 @@ buildscript {
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730'
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
classpath "com.likethesalad.android:stem-plugin:2.2.3"
- classpath 'org.owasp:dependency-check-gradle:7.4.1'
+ classpath 'org.owasp:dependency-check-gradle:7.4.3'
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20"
classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
diff --git a/changelog.d/7864.wip b/changelog.d/7864.wip
new file mode 100644
index 0000000000..4dc55708be
--- /dev/null
+++ b/changelog.d/7864.wip
@@ -0,0 +1 @@
+[Poll] Render active polls list of a room
diff --git a/changelog.d/7879.bugfix b/changelog.d/7879.bugfix
new file mode 100644
index 0000000000..be828ec2cc
--- /dev/null
+++ b/changelog.d/7879.bugfix
@@ -0,0 +1 @@
+Reduce number of crypto database transactions when handling the sync response
diff --git a/changelog.d/7899.bugfix b/changelog.d/7899.bugfix
new file mode 100644
index 0000000000..d95af29d8d
--- /dev/null
+++ b/changelog.d/7899.bugfix
@@ -0,0 +1 @@
+[Voice Broadcast] Stop listening if we reach the last received chunk and there is no last sequence number
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 08bc7ced09..62fa4f5697 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -2335,6 +2335,7 @@
- "One person"
- "%1$d people"
+ Poll history
Uploads
Leave Room
Leave
@@ -3192,6 +3193,8 @@
Closed poll
Results are only revealed when you end the poll
Ended the poll.
+ Active polls
+ There are no active polls in this room
Share location
@@ -3507,4 +3510,7 @@
created a poll.
ended a poll.
Ended poll
+
+ Access Token
+ Your access token gives full access to your account. Do not share it with anyone.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserIdentity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserIdentity.kt
new file mode 100644
index 0000000000..071db7f902
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserIdentity.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 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.crypto.crosssigning
+
+/**
+ * Container for the three cross signing keys: master, self signing and user signing.
+ */
+data class UserIdentity(
+ val masterKey: CryptoCrossSigningKey?,
+ val selfSigningKey: CryptoCrossSigningKey?,
+ val userSigningKey: CryptoCrossSigningKey?,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
index 7862da1c17..50497e3a27 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
@@ -89,6 +89,7 @@ import org.matrix.android.sdk.internal.crypto.model.SessionInfo
import org.matrix.android.sdk.internal.crypto.model.toRest
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator
import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
@@ -192,21 +193,21 @@ internal class DefaultCryptoService @Inject constructor(
private val isStarting = AtomicBoolean(false)
private val isStarted = AtomicBoolean(false)
- fun onStateEvent(roomId: String, event: Event) {
+ fun onStateEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) {
when (event.type) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
- EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
+ EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator)
}
}
- fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean) {
+ fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean, cryptoStoreAggregator: CryptoStoreAggregator?) {
// handle state events
if (event.isStateEvent()) {
when (event.type) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
- EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
+ EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator)
}
}
@@ -430,8 +431,10 @@ internal class DefaultCryptoService @Inject constructor(
* A sync response has been received.
*
* @param syncResponse the syncResponse
+ * @param cryptoStoreAggregator data aggregated during the sync response treatment to store
*/
- fun onSyncCompleted(syncResponse: SyncResponse) {
+ fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) {
+ cryptoStore.storeData(cryptoStoreAggregator)
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching {
if (syncResponse.deviceLists != null) {
@@ -998,15 +1001,26 @@ internal class DefaultCryptoService @Inject constructor(
}
}
- private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) {
+ private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) {
if (!event.isStateEvent()) return
val eventContent = event.content.toModel()
val historyVisibility = eventContent?.historyVisibility
if (historyVisibility == null) {
- cryptoStore.setShouldShareHistory(roomId, false)
+ if (cryptoStoreAggregator != null) {
+ cryptoStoreAggregator.setShouldShareHistoryData[roomId] = false
+ } else {
+ // Store immediately
+ cryptoStore.setShouldShareHistory(roomId, false)
+ }
} else {
- cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED)
- cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory())
+ if (cryptoStoreAggregator != null) {
+ cryptoStoreAggregator.setShouldEncryptForInvitedMembersData[roomId] = historyVisibility != RoomHistoryVisibility.JOINED
+ cryptoStoreAggregator.setShouldShareHistoryData[roomId] = historyVisibility.shouldShareHistory()
+ } else {
+ // Store immediately
+ cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED)
+ cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory())
+ }
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
index 7e9e156003..364d77f7ac 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
@@ -25,11 +25,13 @@ import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.extensions.measureMetric
import org.matrix.android.sdk.api.metrics.DownloadDeviceKeysMetricsPlugin
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import org.matrix.android.sdk.internal.crypto.store.UserDataToStore
import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
@@ -371,6 +373,8 @@ internal class DeviceListManager @Inject constructor(
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
}
+ val userDataToStore = UserDataToStore()
+
for (userId in filteredUsers) {
// al devices =
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
@@ -404,7 +408,7 @@ internal class DeviceListManager @Inject constructor(
}
// Update the store
// Note that devices which aren't in the response will be removed from the stores
- cryptoStore.storeUserDevices(userId, workingCopy)
+ userDataToStore.userDevices[userId] = workingCopy
}
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
@@ -416,14 +420,15 @@ internal class DeviceListManager @Inject constructor(
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
}
- cryptoStore.storeUserCrossSigningKeys(
- userId,
- masterKey,
- selfSigningKey,
- userSigningKey
+ userDataToStore.userIdentities[userId] = UserIdentity(
+ masterKey = masterKey,
+ selfSigningKey = selfSigningKey,
+ userSigningKey = userSigningKey
)
}
+ cryptoStore.storeData(userDataToStore)
+
// Update devices trust for these users
// dispatchDeviceChange(downloadUsers)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
index 21e3342365..0305f73a7b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
@@ -22,9 +22,9 @@ import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
-import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
+import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
@@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
+import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmOutboundGroupSession
@@ -230,11 +231,12 @@ internal interface IMXCryptoStore {
*/
fun storeUserDevices(userId: String, devices: Map?)
- fun storeUserCrossSigningKeys(
+ /**
+ * Store the cross signing keys for the user userId.
+ */
+ fun storeUserIdentity(
userId: String,
- masterKey: CryptoCrossSigningKey?,
- selfSigningKey: CryptoCrossSigningKey?,
- userSigningKey: CryptoCrossSigningKey?
+ userIdentity: UserIdentity
)
/**
@@ -290,6 +292,13 @@ internal interface IMXCryptoStore {
fun shouldEncryptForInvitedMembers(roomId: String): Boolean
+ /**
+ * Sets a boolean flag that will determine whether or not this device should encrypt Events for
+ * invited members.
+ *
+ * @param roomId the room id
+ * @param shouldEncryptForInvitedMembers The boolean flag
+ */
fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean)
fun shouldShareHistory(roomId: String): Boolean
@@ -580,4 +589,14 @@ internal interface IMXCryptoStore {
fun areDeviceKeysUploaded(): Boolean
fun tidyUpDataBase()
fun getOutgoingRoomKeyRequests(inStates: Set): List
+
+ /**
+ * Store a bunch of data collected during a sync response treatment. @See [CryptoStoreAggregator].
+ */
+ fun storeData(cryptoStoreAggregator: CryptoStoreAggregator)
+
+ /**
+ * Store a bunch of data related to the users. @See [UserDataToStore].
+ */
+ fun storeData(userDataToStore: UserDataToStore)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/UserDataToStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/UserDataToStore.kt
new file mode 100644
index 0000000000..914ce4704e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/UserDataToStore.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2023 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.crypto.store
+
+import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+
+internal data class UserDataToStore(
+ /**
+ * Map of userId -> (Map of deviceId -> [CryptoDeviceInfo]).
+ */
+ val userDevices: MutableMap> = mutableMapOf(),
+ /**
+ * Map of userId -> [UserIdentity].
+ */
+ val userIdentities: MutableMap = mutableMapOf(),
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/CryptoStoreAggregator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/CryptoStoreAggregator.kt
new file mode 100644
index 0000000000..687ec95ec3
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/CryptoStoreAggregator.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2023 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.crypto.store.db
+
+data class CryptoStoreAggregator(
+ val setShouldShareHistoryData: MutableMap = mutableMapOf(),
+ val setShouldEncryptForInvitedMembersData: MutableMap = mutableMapOf(),
+) {
+ fun isEmpty(): Boolean {
+ return setShouldShareHistoryData.isEmpty() &&
+ setShouldEncryptForInvitedMembersData.isEmpty()
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt
index 2d66ce1488..6412df205f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt
@@ -20,10 +20,12 @@ import android.util.Base64
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmObject
+import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.io.ObjectOutputStream
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
+import kotlin.system.measureTimeMillis
/**
* Get realm, invoke the action, close realm, and return the result of the action.
@@ -55,10 +57,12 @@ internal fun doRealmQueryAndCopyList(realmConfiguration: Realm
/**
* Get realm instance, invoke the action in a transaction and close realm.
*/
-internal fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
- Realm.getInstance(realmConfiguration).use { realm ->
- realm.executeTransaction { action.invoke(it) }
- }
+internal fun doRealmTransaction(tag: String, realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
+ measureTimeMillis {
+ Realm.getInstance(realmConfiguration).use { realm ->
+ realm.executeTransaction { action.invoke(it) }
+ }
+ }.also { Timber.w("doRealmTransaction for $tag took $it millis") }
}
internal fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
index 1b52b79746..b4368467a2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
@@ -33,9 +33,9 @@ import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
-import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
+import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
@@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrappe
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import org.matrix.android.sdk.internal.crypto.store.UserDataToStore
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
@@ -147,7 +148,7 @@ internal class RealmCryptoStore @Inject constructor(
init {
// Ensure CryptoMetadataEntity is inserted in DB
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("init", realmConfiguration) { realm ->
var currentMetadata = realm.where().findFirst()
var deleteAll = false
@@ -189,7 +190,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun deleteStore() {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("deleteStore", realmConfiguration) {
it.deleteAll()
}
}
@@ -218,7 +219,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun storeDeviceId(deviceId: String) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("storeDeviceId", realmConfiguration) {
it.where().findFirst()?.deviceId = deviceId
}
}
@@ -230,7 +231,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun saveOlmAccount() {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("saveOlmAccount", realmConfiguration) {
it.where().findFirst()?.putOlmAccount(olmAccount)
}
}
@@ -248,7 +249,7 @@ internal class RealmCryptoStore @Inject constructor(
@Synchronized
override fun getOrCreateOlmAccount(): OlmAccount {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("getOrCreateOlmAccount", realmConfiguration) {
val metaData = it.where().findFirst()
val existing = metaData!!.getOlmAccount()
if (existing == null) {
@@ -288,129 +289,139 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun storeUserDevices(userId: String, devices: Map?) {
- doRealmTransaction(realmConfiguration) { realm ->
- if (devices == null) {
- Timber.d("Remove user $userId")
- // Remove the user
- UserEntity.delete(realm, userId)
- } else {
- val userEntity = UserEntity.getOrCreate(realm, userId)
- // First delete the removed devices
- val deviceIds = devices.keys
- userEntity.devices.toTypedArray().iterator().let {
- while (it.hasNext()) {
- val deviceInfoEntity = it.next()
- if (deviceInfoEntity.deviceId !in deviceIds) {
- Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId")
- deviceInfoEntity.deleteOnCascade()
- }
+ doRealmTransaction("storeUserDevices", realmConfiguration) { realm ->
+ storeUserDevices(realm, userId, devices)
+ }
+ }
+
+ private fun storeUserDevices(realm: Realm, userId: String, devices: Map?) {
+ if (devices == null) {
+ Timber.d("Remove user $userId")
+ // Remove the user
+ UserEntity.delete(realm, userId)
+ } else {
+ val userEntity = UserEntity.getOrCreate(realm, userId)
+ // First delete the removed devices
+ val deviceIds = devices.keys
+ userEntity.devices.toTypedArray().iterator().let {
+ while (it.hasNext()) {
+ val deviceInfoEntity = it.next()
+ if (deviceInfoEntity.deviceId !in deviceIds) {
+ Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId")
+ deviceInfoEntity.deleteOnCascade()
}
}
- // Then update existing devices or add new one
- devices.values.forEach { cryptoDeviceInfo ->
- val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId }
- if (existingDeviceInfoEntity == null) {
- // Add the device
- Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId")
- val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo)
- newEntity.firstTimeSeenLocalTs = clock.epochMillis()
- userEntity.devices.add(newEntity)
- } else {
- // Update the device
- Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId")
- CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo)
- }
+ }
+ // Then update existing devices or add new one
+ devices.values.forEach { cryptoDeviceInfo ->
+ val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId }
+ if (existingDeviceInfoEntity == null) {
+ // Add the device
+ Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId")
+ val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo)
+ newEntity.firstTimeSeenLocalTs = clock.epochMillis()
+ userEntity.devices.add(newEntity)
+ } else {
+ // Update the device
+ Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId")
+ CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo)
}
}
}
}
- override fun storeUserCrossSigningKeys(
+ override fun storeUserIdentity(
userId: String,
- masterKey: CryptoCrossSigningKey?,
- selfSigningKey: CryptoCrossSigningKey?,
- userSigningKey: CryptoCrossSigningKey?
+ userIdentity: UserIdentity,
) {
- doRealmTransaction(realmConfiguration) { realm ->
- UserEntity.getOrCreate(realm, userId)
- .let { userEntity ->
- if (masterKey == null || selfSigningKey == null) {
- // The user has disabled cross signing?
- userEntity.crossSigningInfoEntity?.deleteOnCascade()
- userEntity.crossSigningInfoEntity = null
- } else {
- var shouldResetMyDevicesLocalTrust = false
- CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo ->
- // What should we do if we detect a change of the keys?
- val existingMaster = signingInfo.getMasterKey()
- if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) {
- crossSigningKeysMapper.update(existingMaster, masterKey)
- } else {
- Timber.d("## CrossSigning MSK change for $userId")
- val keyEntity = crossSigningKeysMapper.map(masterKey)
- signingInfo.setMasterKey(keyEntity)
- if (userId == this.userId) {
- shouldResetMyDevicesLocalTrust = true
- // my msk has changed! clear my private key
- // Could we have some race here? e.g I am the one that did change the keys
- // could i get this update to early and clear the private keys?
- // -> initializeCrossSigning is guarding for that by storing all at once
- realm.where().findFirst()?.apply {
- xSignMasterPrivateKey = null
- }
+ doRealmTransaction("storeUserIdentity", realmConfiguration) { realm ->
+ storeUserIdentity(realm, userId, userIdentity)
+ }
+ }
+
+ private fun storeUserIdentity(
+ realm: Realm,
+ userId: String,
+ userIdentity: UserIdentity,
+ ) {
+ UserEntity.getOrCreate(realm, userId)
+ .let { userEntity ->
+ if (userIdentity.masterKey == null || userIdentity.selfSigningKey == null) {
+ // The user has disabled cross signing?
+ userEntity.crossSigningInfoEntity?.deleteOnCascade()
+ userEntity.crossSigningInfoEntity = null
+ } else {
+ var shouldResetMyDevicesLocalTrust = false
+ CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo ->
+ // What should we do if we detect a change of the keys?
+ val existingMaster = signingInfo.getMasterKey()
+ if (existingMaster != null && existingMaster.publicKeyBase64 == userIdentity.masterKey.unpaddedBase64PublicKey) {
+ crossSigningKeysMapper.update(existingMaster, userIdentity.masterKey)
+ } else {
+ Timber.d("## CrossSigning MSK change for $userId")
+ val keyEntity = crossSigningKeysMapper.map(userIdentity.masterKey)
+ signingInfo.setMasterKey(keyEntity)
+ if (userId == this.userId) {
+ shouldResetMyDevicesLocalTrust = true
+ // my msk has changed! clear my private key
+ // Could we have some race here? e.g I am the one that did change the keys
+ // could i get this update to early and clear the private keys?
+ // -> initializeCrossSigning is guarding for that by storing all at once
+ realm.where().findFirst()?.apply {
+ xSignMasterPrivateKey = null
}
}
-
- val existingSelfSigned = signingInfo.getSelfSignedKey()
- if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) {
- crossSigningKeysMapper.update(existingSelfSigned, selfSigningKey)
- } else {
- Timber.d("## CrossSigning SSK change for $userId")
- val keyEntity = crossSigningKeysMapper.map(selfSigningKey)
- signingInfo.setSelfSignedKey(keyEntity)
- if (userId == this.userId) {
- shouldResetMyDevicesLocalTrust = true
- // my ssk has changed! clear my private key
- realm.where().findFirst()?.apply {
- xSignSelfSignedPrivateKey = null
- }
- }
- }
-
- // Only for me
- if (userSigningKey != null) {
- val existingUSK = signingInfo.getUserSigningKey()
- if (existingUSK != null && existingUSK.publicKeyBase64 == userSigningKey.unpaddedBase64PublicKey) {
- crossSigningKeysMapper.update(existingUSK, userSigningKey)
- } else {
- Timber.d("## CrossSigning USK change for $userId")
- val keyEntity = crossSigningKeysMapper.map(userSigningKey)
- signingInfo.setUserSignedKey(keyEntity)
- if (userId == this.userId) {
- shouldResetMyDevicesLocalTrust = true
- // my usk has changed! clear my private key
- realm.where().findFirst()?.apply {
- xSignUserPrivateKey = null
- }
- }
- }
- }
-
- // When my cross signing keys are reset, we consider clearing all existing device trust
- if (shouldResetMyDevicesLocalTrust) {
- realm.where()
- .equalTo(UserEntityFields.USER_ID, this.userId)
- .findFirst()
- ?.devices?.forEach {
- it?.trustLevelEntity?.crossSignedVerified = false
- it?.trustLevelEntity?.locallyVerified = it.deviceId == deviceId
- }
- }
- userEntity.crossSigningInfoEntity = signingInfo
}
+
+ val existingSelfSigned = signingInfo.getSelfSignedKey()
+ if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == userIdentity.selfSigningKey.unpaddedBase64PublicKey) {
+ crossSigningKeysMapper.update(existingSelfSigned, userIdentity.selfSigningKey)
+ } else {
+ Timber.d("## CrossSigning SSK change for $userId")
+ val keyEntity = crossSigningKeysMapper.map(userIdentity.selfSigningKey)
+ signingInfo.setSelfSignedKey(keyEntity)
+ if (userId == this.userId) {
+ shouldResetMyDevicesLocalTrust = true
+ // my ssk has changed! clear my private key
+ realm.where().findFirst()?.apply {
+ xSignSelfSignedPrivateKey = null
+ }
+ }
+ }
+
+ // Only for me
+ if (userIdentity.userSigningKey != null) {
+ val existingUSK = signingInfo.getUserSigningKey()
+ if (existingUSK != null && existingUSK.publicKeyBase64 == userIdentity.userSigningKey.unpaddedBase64PublicKey) {
+ crossSigningKeysMapper.update(existingUSK, userIdentity.userSigningKey)
+ } else {
+ Timber.d("## CrossSigning USK change for $userId")
+ val keyEntity = crossSigningKeysMapper.map(userIdentity.userSigningKey)
+ signingInfo.setUserSignedKey(keyEntity)
+ if (userId == this.userId) {
+ shouldResetMyDevicesLocalTrust = true
+ // my usk has changed! clear my private key
+ realm.where().findFirst()?.apply {
+ xSignUserPrivateKey = null
+ }
+ }
+ }
+ }
+
+ // When my cross signing keys are reset, we consider clearing all existing device trust
+ if (shouldResetMyDevicesLocalTrust) {
+ realm.where()
+ .equalTo(UserEntityFields.USER_ID, this.userId)
+ .findFirst()
+ ?.devices?.forEach {
+ it?.trustLevelEntity?.crossSignedVerified = false
+ it?.trustLevelEntity?.locallyVerified = it.deviceId == deviceId
+ }
+ }
+ userEntity.crossSigningInfoEntity = signingInfo
}
}
- }
+ }
}
override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
@@ -480,7 +491,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}")
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("storePrivateKeysInfo", realmConfiguration) { realm ->
realm.where().findFirst()?.apply {
xSignMasterPrivateKey = msk
xSignUserPrivateKey = usk
@@ -490,7 +501,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("saveBackupRecoveryKey", realmConfiguration) { realm ->
realm.where().findFirst()?.apply {
keyBackupRecoveryKey = recoveryKey
keyBackupRecoveryKeyVersion = version
@@ -516,7 +527,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun storeMSKPrivateKey(msk: String?) {
Timber.v("## CRYPTO | *** storeMSKPrivateKey ${msk != null} ")
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("storeMSKPrivateKey", realmConfiguration) { realm ->
realm.where().findFirst()?.apply {
xSignMasterPrivateKey = msk
}
@@ -525,7 +536,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun storeSSKPrivateKey(ssk: String?) {
Timber.v("## CRYPTO | *** storeSSKPrivateKey ${ssk != null} ")
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("storeSSKPrivateKey", realmConfiguration) { realm ->
realm.where().findFirst()?.apply {
xSignSelfSignedPrivateKey = ssk
}
@@ -534,7 +545,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun storeUSKPrivateKey(usk: String?) {
Timber.v("## CRYPTO | *** storeUSKPrivateKey ${usk != null} ")
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("storeUSKPrivateKey", realmConfiguration) { realm ->
realm.where().findFirst()?.apply {
xSignUserPrivateKey = usk
}
@@ -667,7 +678,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun storeRoomAlgorithm(roomId: String, algorithm: String?) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("storeRoomAlgorithm", realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).let { entity ->
entity.algorithm = algorithm
// store anyway the new algorithm, but mark the room
@@ -708,7 +719,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("setShouldEncryptForInvitedMembers", realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers
}
}
@@ -716,7 +727,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) {
Timber.tag(loggerTag.value)
.v("setShouldShareHistory for room $roomId is $shouldShareHistory")
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("setShouldShareHistory", realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory
}
}
@@ -733,7 +744,7 @@ internal class RealmCryptoStore @Inject constructor(
if (sessionIdentifier != null) {
val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey)
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("storeSession", realmConfiguration) {
val realmOlmSession = OlmSessionEntity().apply {
primaryKey = key
sessionId = sessionIdentifier
@@ -790,7 +801,7 @@ internal class RealmCryptoStore @Inject constructor(
return
}
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("storeInboundGroupSessions", realmConfiguration) { realm ->
sessions.forEach { wrapper ->
val sessionIdentifier = try {
@@ -914,7 +925,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun removeInboundGroupSession(sessionId: String, senderKey: String) {
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("removeInboundGroupSession", realmConfiguration) {
it.where()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findAll()
@@ -933,7 +944,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setKeyBackupVersion(keyBackupVersion: String?) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("setKeyBackupVersion", realmConfiguration) {
it.where().findFirst()?.backupVersion = keyBackupVersion
}
}
@@ -945,7 +956,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("setKeysBackupData", realmConfiguration) {
if (keysBackupData == null) {
// Clear the table
it.where()
@@ -959,7 +970,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun resetBackupMarkers() {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("resetBackupMarkers", realmConfiguration) {
it.where()
.findAll()
.map { inboundGroupSession ->
@@ -973,7 +984,7 @@ internal class RealmCryptoStore @Inject constructor(
return
}
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("markBackupDoneForInboundGroupSessions", realmConfiguration) { realm ->
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
try {
val sessionIdentifier =
@@ -1032,13 +1043,13 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("setGlobalBlacklistUnverifiedDevices", realmConfiguration) {
it.where().findFirst()?.globalBlacklistUnverifiedDevices = block
}
}
override fun enableKeyGossiping(enable: Boolean) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("enableKeyGossiping", realmConfiguration) {
it.where().findFirst()?.globalEnableKeyGossiping = enable
}
}
@@ -1062,13 +1073,13 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun enableShareKeyOnInvite(enable: Boolean) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("enableShareKeyOnInvite", realmConfiguration) {
it.where().findFirst()?.enableKeyForwardingOnInvite = enable
}
}
override fun setDeviceKeysUploaded(uploaded: Boolean) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("setDeviceKeysUploaded", realmConfiguration) {
it.where().findFirst()?.deviceKeysSentToServer = uploaded
}
}
@@ -1115,7 +1126,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("blockUnverifiedDevicesInRoom", realmConfiguration) { realm ->
CryptoRoomEntity.getById(realm, roomId)
?.blacklistUnverifiedDevices = block
}
@@ -1135,7 +1146,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("saveDeviceTrackingStatuses", realmConfiguration) {
deviceTrackingStatuses
.map { entry ->
UserEntity.getOrCreate(it, entry.key)
@@ -1268,7 +1279,7 @@ internal class RealmCryptoStore @Inject constructor(
): OutgoingKeyRequest {
// Insert the request and return the one passed in parameter
lateinit var request: OutgoingKeyRequest
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("getOrAddOutgoingRoomKeyRequest", realmConfiguration) { realm ->
val existing = realm.where()
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId)
@@ -1306,7 +1317,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("updateOutgoingRoomKeyRequestState", realmConfiguration) { realm ->
realm.where()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.apply {
@@ -1320,7 +1331,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("updateOutgoingRoomKeyRequiredIndex", realmConfiguration) { realm ->
realm.where()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.apply {
@@ -1337,7 +1348,7 @@ internal class RealmCryptoStore @Inject constructor(
fromDevice: String?,
event: Event
) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("updateOutgoingRoomKeyReply", realmConfiguration) { realm ->
realm.where()
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId)
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId)
@@ -1353,7 +1364,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun deleteOutgoingRoomKeyRequest(requestId: String) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("deleteOutgoingRoomKeyRequest", realmConfiguration) { realm ->
realm.where()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.deleteOnCascade()
@@ -1361,7 +1372,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("deleteOutgoingRoomKeyRequestInState", realmConfiguration) { realm ->
realm.where()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, state.name)
.findAll()
@@ -1497,7 +1508,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("setMyCrossSigningInfo", realmConfiguration) { realm ->
realm.where().findFirst()?.userId?.let { userId ->
addOrUpdateCrossSigningInfo(realm, userId, info)
}
@@ -1505,7 +1516,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("setUserKeysAsTrusted", realmConfiguration) { realm ->
val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()
@@ -1525,7 +1536,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("setDeviceTrust", realmConfiguration) { realm ->
realm.where(DeviceInfoEntity::class.java)
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
.findFirst()?.let { deviceInfoEntity ->
@@ -1545,7 +1556,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun clearOtherUserTrust() {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("clearOtherUserTrust", realmConfiguration) { realm ->
val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java)
.findAll()
xInfoEntities?.forEach { info ->
@@ -1560,7 +1571,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun updateUsersTrust(check: (String) -> Boolean) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("updateUsersTrust", realmConfiguration) { realm ->
val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java)
.findAll()
xInfoEntities?.forEach { xInfoEntity ->
@@ -1668,13 +1679,13 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("setCrossSigningInfo", realmConfiguration) { realm ->
addOrUpdateCrossSigningInfo(realm, userId, info)
}
}
override fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("markMyMasterKeyAsLocallyTrusted", realmConfiguration) { realm ->
realm.where().findFirst()?.userId?.let { myUserId ->
CrossSigningInfoEntity.get(realm, myUserId)?.getMasterKey()?.let { xInfoEntity ->
val level = xInfoEntity.trustLevelEntity
@@ -1713,7 +1724,7 @@ internal class RealmCryptoStore @Inject constructor(
val roomId = withHeldContent.roomId ?: return
val sessionId = withHeldContent.sessionId ?: return
if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("addWithHeldMegolmSession", realmConfiguration) { realm ->
WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let {
it.code = withHeldContent.code
it.senderKey = withHeldContent.senderKey
@@ -1745,7 +1756,7 @@ internal class RealmCryptoStore @Inject constructor(
deviceIdentityKey: String,
chainIndex: Int
) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("markedSessionAsShared", realmConfiguration) { realm ->
SharedSessionEntity.create(
realm = realm,
roomId = roomId,
@@ -1794,7 +1805,7 @@ internal class RealmCryptoStore @Inject constructor(
*/
override fun tidyUpDataBase() {
val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("tidyUpDataBase", realmConfiguration) { realm ->
// Clean the old ones?
realm.where()
@@ -1815,4 +1826,31 @@ internal class RealmCryptoStore @Inject constructor(
// Can we do something for WithHeldSessionEntity?
}
}
+
+ override fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) {
+ if (cryptoStoreAggregator.isEmpty()) {
+ return
+ }
+ doRealmTransaction("storeData - CryptoStoreAggregator", realmConfiguration) { realm ->
+ // setShouldShareHistory
+ cryptoStoreAggregator.setShouldShareHistoryData.forEach {
+ CryptoRoomEntity.getOrCreate(realm, it.key).shouldShareHistory = it.value
+ }
+ // setShouldEncryptForInvitedMembers
+ cryptoStoreAggregator.setShouldEncryptForInvitedMembersData.forEach {
+ CryptoRoomEntity.getOrCreate(realm, it.key).shouldEncryptForInvitedMembers = it.value
+ }
+ }
+ }
+
+ override fun storeData(userDataToStore: UserDataToStore) {
+ doRealmTransaction("storeData - UserDataToStore", realmConfiguration) { realm ->
+ userDataToStore.userDevices.forEach {
+ storeUserDevices(realm, it.key, it.value)
+ }
+ userDataToStore.userIdentities.forEach {
+ storeUserIdentity(realm, it.key, it.value)
+ }
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt
index cfc26045a0..ce34b0430e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt
@@ -42,14 +42,12 @@ internal class StreamEventsManager @Inject constructor() {
listeners.remove(listener)
}
- fun dispatchLiveEventReceived(event: Event, roomId: String, initialSync: Boolean) {
+ fun dispatchLiveEventReceived(event: Event, roomId: String) {
Timber.v("## dispatchLiveEventReceived ${event.eventId}")
coroutineScope.launch {
- if (!initialSync) {
- listeners.forEach {
- tryOrNull {
- it.onLiveEvent(roomId, event)
- }
+ listeners.forEach {
+ tryOrNull {
+ it.onLiveEvent(roomId, event)
}
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
index 793c2573be..653069b3c8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
@@ -176,7 +176,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
}
// Give info to crypto module
- cryptoService.onStateEvent(roomId, event)
+ cryptoService.onStateEvent(roomId, event, null)
}
roomMemberContentsByUser.getOrPut(event.senderId) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
index 05d50d9595..cb407bb1cb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
import org.matrix.android.sdk.api.session.sync.model.SyncResponse
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
+import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.session.SessionListeners
@@ -92,7 +93,7 @@ internal class SyncResponseHandler @Inject constructor(
postTreatmentSyncResponse(syncResponse, isInitialSync)
- markCryptoSyncCompleted(syncResponse)
+ markCryptoSyncCompleted(syncResponse, aggregator.cryptoStoreAggregator)
handlePostSync()
@@ -218,10 +219,10 @@ internal class SyncResponseHandler @Inject constructor(
}
}
- private fun markCryptoSyncCompleted(syncResponse: SyncResponse) {
+ private fun markCryptoSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) {
relevantPlugins.measureSpan("task", "crypto_sync_handler_onSyncCompleted") {
measureTimeMillis {
- cryptoSyncHandler.onSyncCompleted(syncResponse)
+ cryptoSyncHandler.onSyncCompleted(syncResponse, cryptoStoreAggregator)
}.also {
Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms")
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt
index 2b7f936fa8..af05e08da3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt
@@ -16,6 +16,8 @@
package org.matrix.android.sdk.internal.session.sync
+import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator
+
internal class SyncResponsePostTreatmentAggregator {
// List of RoomId
val ephemeralFilesToDelete = mutableListOf()
@@ -28,4 +30,7 @@ internal class SyncResponsePostTreatmentAggregator {
// Set of users to call `crossSigningService.checkTrustAndAffectedRoomShields` once per sync
val userIdsForCheckingTrustAndAffectedRoomShields = mutableSetOf()
+
+ // For the crypto store
+ val cryptoStoreAggregator = CryptoStoreAggregator()
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
index 551db52dbd..7224b0c29c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.sync.model.SyncResponse
import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
+import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator
import org.matrix.android.sdk.internal.crypto.tasks.toDeviceTracingId
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
import org.matrix.android.sdk.internal.session.sync.ProgressReporter
@@ -85,8 +86,8 @@ internal class CryptoSyncHandler @Inject constructor(
}
}
- fun onSyncCompleted(syncResponse: SyncResponse) {
- cryptoService.onSyncCompleted(syncResponse)
+ fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) {
+ cryptoService.onSyncCompleted(syncResponse, cryptoStoreAggregator)
}
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
index 4001ae2ccf..5e4886ce1e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
@@ -258,7 +258,7 @@ internal class RoomSyncHandler @Inject constructor(
root = eventEntity
}
// Give info to crypto module
- cryptoService.onStateEvent(roomId, event)
+ cryptoService.onStateEvent(roomId, event, aggregator.cryptoStoreAggregator)
roomMemberEventHandler.handle(realm, roomId, event, isInitialSync, aggregator)
}
}
@@ -376,8 +376,15 @@ internal class RoomSyncHandler @Inject constructor(
roomEntity.chunks.clearWith { it.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) }
roomTypingUsersHandler.handle(realm, roomId, null)
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE)
- roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary,
- roomSync.unreadNotifications, roomSync.unreadThreadNotifications, aggregator = aggregator)
+ roomSummaryUpdater.update(
+ realm,
+ roomId,
+ membership,
+ roomSync.summary,
+ roomSync.unreadNotifications,
+ roomSync.unreadThreadNotifications,
+ aggregator = aggregator,
+ )
return roomEntity
}
@@ -423,7 +430,9 @@ internal class RoomSyncHandler @Inject constructor(
val isInitialSync = insertType == EventInsertType.INITIAL_SYNC
eventIds.add(event.eventId)
- liveEventService.get().dispatchLiveEventReceived(event, roomId, isInitialSync)
+ if (!isInitialSync) {
+ liveEventService.get().dispatchLiveEventReceived(event, roomId)
+ }
if (event.isEncrypted() && !isInitialSync) {
try {
@@ -486,7 +495,7 @@ internal class RoomSyncHandler @Inject constructor(
}
}
// Give info to crypto module
- cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync)
+ cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync, aggregator.cryptoStoreAggregator)
// Try to remove local echo
event.unsignedData?.transactionId?.also { txId ->
diff --git a/vector/build.gradle b/vector/build.gradle
index 83af7ecc04..91d2a8c46a 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -308,7 +308,7 @@ dependencies {
// Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868
// Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0)
//noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26.
- implementation "org.checkerframework:checker:3.27.0"
+ implementation "org.checkerframework:checker:3.29.0"
androidTestImplementation libs.androidx.testCore
androidTestImplementation libs.androidx.testRunner
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index d22ab51e7a..911bbfa4a3 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -84,6 +84,7 @@ import im.vector.app.features.roomprofile.banned.RoomBannedMemberListViewModel
import im.vector.app.features.roomprofile.members.RoomMemberListViewModel
import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsViewModel
import im.vector.app.features.roomprofile.permissions.RoomPermissionsViewModel
+import im.vector.app.features.roomprofile.polls.RoomPollsViewModel
import im.vector.app.features.roomprofile.settings.RoomSettingsViewModel
import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel
import im.vector.app.features.roomprofile.uploads.RoomUploadsViewModel
@@ -697,4 +698,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(SetLinkViewModel::class)
fun setLinkViewModelFactory(factory: SetLinkViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
+ @Binds
+ @IntoMap
+ @MavericksViewModelKey(RoomPollsViewModel::class)
+ fun roomPollsViewModelFactory(factory: RoomPollsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
index 526d676dee..3c37c92650 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
@@ -36,6 +36,7 @@ import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment
import im.vector.app.features.roomprofile.members.RoomMemberListFragment
import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsFragment
import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment
+import im.vector.app.features.roomprofile.polls.RoomPollsFragment
import im.vector.app.features.roomprofile.settings.RoomSettingsFragment
import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment
import im.vector.lib.core.utils.compat.getParcelableCompat
@@ -98,6 +99,7 @@ class RoomProfileActivity :
RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings()
RoomProfileSharedAction.OpenRoomAliasesSettings -> openRoomAlias()
RoomProfileSharedAction.OpenRoomPermissionsSettings -> openRoomPermissions()
+ RoomProfileSharedAction.OpenRoomPolls -> openRoomPolls()
RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads()
RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers()
RoomProfileSharedAction.OpenRoomNotificationSettings -> openRoomNotificationSettings()
@@ -126,6 +128,10 @@ class RoomProfileActivity :
finish()
}
+ private fun openRoomPolls() {
+ addFragmentToBackstack(views.simpleFragmentContainer, RoomPollsFragment::class.java, roomProfileArgs)
+ }
+
private fun openRoomUploads() {
addFragmentToBackstack(views.simpleFragmentContainer, RoomUploadsFragment::class.java, roomProfileArgs)
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
index eb43a345f2..30bd6c7ed3 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
@@ -18,6 +18,7 @@
package im.vector.app.features.roomprofile
import com.airbnb.epoxy.TypedEpoxyController
+import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.epoxy.expandableTextItem
import im.vector.app.core.epoxy.profiles.buildProfileAction
@@ -56,6 +57,7 @@ class RoomProfileController @Inject constructor(
fun onMemberListClicked()
fun onBannedMemberListClicked()
fun onNotificationsClicked()
+ fun onPollHistoryClicked()
fun onUploadsClicked()
fun createShortcut()
fun onSettingsClicked()
@@ -263,6 +265,15 @@ class RoomProfileController @Inject constructor(
action = { callback?.onBannedMemberListClicked() }
)
}
+ if (BuildConfig.DEBUG) {
+ // WIP, will be in release when related screens will be finished
+ buildProfileAction(
+ id = "poll_history",
+ title = stringProvider.getString(R.string.room_profile_section_more_polls),
+ icon = R.drawable.ic_attachment_poll,
+ action = { callback?.onPollHistoryClicked() }
+ )
+ }
buildProfileAction(
id = "uploads",
title = stringProvider.getString(R.string.room_profile_section_more_uploads),
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
index f4394111ab..51885dbf39 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
@@ -269,6 +269,10 @@ class RoomProfileFragment :
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomNotificationSettings)
}
+ override fun onPollHistoryClicked() {
+ roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomPolls)
+ }
+
override fun onUploadsClicked() {
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomUploads)
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt
index 7d62bb86a1..b243ceb206 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt
@@ -25,6 +25,7 @@ sealed class RoomProfileSharedAction : VectorSharedAction {
object OpenRoomSettings : RoomProfileSharedAction()
object OpenRoomAliasesSettings : RoomProfileSharedAction()
object OpenRoomPermissionsSettings : RoomProfileSharedAction()
+ object OpenRoomPolls : RoomProfileSharedAction()
object OpenRoomUploads : RoomProfileSharedAction()
object OpenRoomMembers : RoomProfileSharedAction()
object OpenBannedRoomMembers : RoomProfileSharedAction()
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt
new file mode 100644
index 0000000000..d35d192e04
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2022 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.roomprofile.polls
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+class GetPollsUseCase @Inject constructor() {
+
+ fun execute(filter: RoomPollsFilterType): Flow> {
+ // TODO unmock and add unit tests
+ return when (filter) {
+ RoomPollsFilterType.ACTIVE -> getActivePolls()
+ RoomPollsFilterType.ENDED -> emptyFlow()
+ }.map { it.sortedByDescending { poll -> poll.creationTimestamp } }
+ }
+
+ private fun getActivePolls(): Flow> {
+ return flowOf(
+ listOf(
+ PollSummary.ActivePoll(
+ id = "id1",
+ // 2022/06/28 UTC+1
+ creationTimestamp = 1656367200000,
+ title = "Which charity would you like to support?"
+ ),
+ PollSummary.ActivePoll(
+ id = "id2",
+ // 2022/06/26 UTC+1
+ creationTimestamp = 1656194400000,
+ title = "Which sport should the pupils do this year?"
+ ),
+ PollSummary.ActivePoll(
+ id = "id3",
+ // 2022/06/24 UTC+1
+ creationTimestamp = 1656021600000,
+ title = "What type of food should we have at the party?"
+ ),
+ PollSummary.ActivePoll(
+ id = "id4",
+ // 2022/06/22 UTC+1
+ creationTimestamp = 1655848800000,
+ title = "What film should we show at the end of the year party?"
+ ),
+ )
+ )
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt
new file mode 100644
index 0000000000..3eb45c6144
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2022 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.roomprofile.polls
+
+sealed interface PollSummary {
+ data class ActivePoll(
+ val id: String,
+ val creationTimestamp: Long,
+ val title: String,
+ ) : PollSummary
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt
new file mode 100644
index 0000000000..5f074bdd6f
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2022 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.roomprofile.polls
+
+import im.vector.app.core.platform.VectorViewModelAction
+
+sealed interface RoomPollsAction : VectorViewModelAction {
+ data class SetFilter(val filter: RoomPollsFilterType) : RoomPollsAction
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsFilterType.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsFilterType.kt
new file mode 100644
index 0000000000..39f1163536
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsFilterType.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2022 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.roomprofile.polls
+
+enum class RoomPollsFilterType {
+ ACTIVE,
+ ENDED,
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsFragment.kt
new file mode 100644
index 0000000000..5c150f4391
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsFragment.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2022 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.roomprofile.polls
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.airbnb.mvrx.args
+import com.airbnb.mvrx.fragmentViewModel
+import com.google.android.material.tabs.TabLayoutMediator
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
+import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.databinding.FragmentRoomPollsBinding
+import im.vector.app.features.roomprofile.RoomProfileArgs
+
+@AndroidEntryPoint
+class RoomPollsFragment : VectorBaseFragment() {
+
+ private val roomProfileArgs: RoomProfileArgs by args()
+
+ private val viewModel: RoomPollsViewModel by fragmentViewModel()
+
+ private var tabLayoutMediator: TabLayoutMediator? = null
+
+ override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomPollsBinding {
+ return FragmentRoomPollsBinding.inflate(inflater, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupToolbar()
+ setupTabs()
+ }
+
+ override fun onDestroyView() {
+ views.roomPollsViewPager.adapter = null
+ tabLayoutMediator?.detach()
+ tabLayoutMediator = null
+ super.onDestroyView()
+ }
+
+ private fun setupToolbar() {
+ setupToolbar(views.roomPollsToolbar)
+ .allowBack()
+ }
+
+ private fun setupTabs() {
+ views.roomPollsViewPager.adapter = RoomPollsPagerAdapter(this)
+
+ tabLayoutMediator = TabLayoutMediator(views.roomPollsTabs, views.roomPollsViewPager) { tab, position ->
+ when (position) {
+ 0 -> tab.text = getString(R.string.room_polls_active)
+ }
+ }.also { it.attach() }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsPagerAdapter.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsPagerAdapter.kt
new file mode 100644
index 0000000000..5472782079
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsPagerAdapter.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 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.roomprofile.polls
+
+import androidx.fragment.app.Fragment
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import im.vector.app.features.roomprofile.polls.active.RoomActivePollsFragment
+
+class RoomPollsPagerAdapter(
+ private val fragment: Fragment
+) : FragmentStateAdapter(fragment) {
+
+ override fun getItemCount() = 1
+
+ override fun createFragment(position: Int): Fragment {
+ return instantiateFragment(RoomActivePollsFragment::class.java.name)
+ }
+
+ private fun instantiateFragment(fragmentName: String): Fragment {
+ return fragment.childFragmentManager.fragmentFactory.instantiate(fragment.requireContext().classLoader, fragmentName)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt
new file mode 100644
index 0000000000..231123563a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2022 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.roomprofile.polls
+
+import im.vector.app.core.platform.VectorViewEvents
+
+sealed class RoomPollsViewEvent : VectorViewEvents
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt
new file mode 100644
index 0000000000..7bc06894fa
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2022 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.roomprofile.polls
+
+import androidx.annotation.VisibleForTesting
+import com.airbnb.mvrx.MavericksViewModelFactory
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.core.di.MavericksAssistedViewModelFactory
+import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.core.platform.VectorViewModel
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+
+class RoomPollsViewModel @AssistedInject constructor(
+ @Assisted initialState: RoomPollsViewState,
+ private val getPollsUseCase: GetPollsUseCase,
+) : VectorViewModel(initialState) {
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: RoomPollsViewState): RoomPollsViewModel
+ }
+
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+
+ @VisibleForTesting
+ var pollsCollectionJob: Job? = null
+
+ override fun handle(action: RoomPollsAction) {
+ when (action) {
+ is RoomPollsAction.SetFilter -> handleSetFilter(action.filter)
+ }
+ }
+
+ override fun onCleared() {
+ pollsCollectionJob = null
+ super.onCleared()
+ }
+
+ private fun handleSetFilter(filter: RoomPollsFilterType) {
+ pollsCollectionJob?.cancel()
+ pollsCollectionJob = getPollsUseCase.execute(filter)
+ .onEach { setState { copy(polls = it) } }
+ .launchIn(viewModelScope)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt
new file mode 100644
index 0000000000..74794c99b1
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 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.roomprofile.polls
+
+import com.airbnb.mvrx.MavericksState
+import im.vector.app.features.roomprofile.RoomProfileArgs
+
+data class RoomPollsViewState(
+ val roomId: String,
+ val polls: List = emptyList(),
+) : MavericksState {
+
+ constructor(roomProfileArgs: RoomProfileArgs) : this(roomId = roomProfileArgs.roomId)
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/ActivePollItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/ActivePollItem.kt
new file mode 100644
index 0000000000..35b1ecd6e1
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/ActivePollItem.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022 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.roomprofile.polls.active
+
+import android.widget.TextView
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.R
+import im.vector.app.core.epoxy.ClickListener
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.core.epoxy.onClick
+
+@EpoxyModelClass
+abstract class ActivePollItem : VectorEpoxyModel(R.layout.item_poll) {
+
+ @EpoxyAttribute
+ lateinit var formattedDate: String
+
+ @EpoxyAttribute
+ lateinit var title: String
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var clickListener: ClickListener? = null
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ holder.view.onClick(clickListener)
+ holder.date.text = formattedDate
+ holder.title.text = title
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val date by bind(R.id.pollActiveDate)
+ val title by bind(R.id.pollActiveTitle)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsController.kt
new file mode 100644
index 0000000000..7a7c818693
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsController.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022 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.roomprofile.polls.active
+
+import com.airbnb.epoxy.TypedEpoxyController
+import im.vector.app.core.date.DateFormatKind
+import im.vector.app.core.date.VectorDateFormatter
+import im.vector.app.features.roomprofile.polls.PollSummary
+import javax.inject.Inject
+
+class RoomActivePollsController @Inject constructor(
+ val dateFormatter: VectorDateFormatter,
+) : TypedEpoxyController>() {
+
+ interface Listener {
+ fun onPollClicked(pollId: String)
+ }
+
+ var listener: Listener? = null
+
+ override fun buildModels(data: List?) {
+ if (data.isNullOrEmpty()) {
+ return
+ }
+
+ val host = this
+ for (poll in data) {
+ activePollItem {
+ id(poll.id)
+ formattedDate(host.dateFormatter.format(poll.creationTimestamp, DateFormatKind.TIMELINE_DAY_DIVIDER))
+ title(poll.title)
+ clickListener {
+ host.listener?.onPollClicked(poll.id)
+ }
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt
new file mode 100644
index 0000000000..61c7e961bd
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2022 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.roomprofile.polls.active
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import com.airbnb.mvrx.parentFragmentViewModel
+import com.airbnb.mvrx.withState
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
+import im.vector.app.core.extensions.cleanup
+import im.vector.app.core.extensions.configureWith
+import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.databinding.FragmentRoomPollsListBinding
+import im.vector.app.features.roomprofile.polls.PollSummary
+import im.vector.app.features.roomprofile.polls.RoomPollsAction
+import im.vector.app.features.roomprofile.polls.RoomPollsFilterType
+import im.vector.app.features.roomprofile.polls.RoomPollsViewModel
+import timber.log.Timber
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class RoomActivePollsFragment :
+ VectorBaseFragment(),
+ RoomActivePollsController.Listener {
+
+ @Inject
+ lateinit var roomActivePollsController: RoomActivePollsController
+
+ private val viewModel: RoomPollsViewModel by parentFragmentViewModel(RoomPollsViewModel::class)
+
+ override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomPollsListBinding {
+ return FragmentRoomPollsListBinding.inflate(inflater, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupList()
+ }
+
+ private fun setupList() {
+ roomActivePollsController.listener = this
+ views.roomPollsList.configureWith(roomActivePollsController)
+ views.roomPollsEmptyTitle.text = getString(R.string.room_polls_active_no_item)
+ }
+
+ override fun onDestroyView() {
+ cleanUpList()
+ super.onDestroyView()
+ }
+
+ private fun cleanUpList() {
+ views.roomPollsList.cleanup()
+ roomActivePollsController.listener = null
+ }
+
+ override fun onResume() {
+ super.onResume()
+ viewModel.handle(RoomPollsAction.SetFilter(RoomPollsFilterType.ACTIVE))
+ }
+
+ override fun invalidate() = withState(viewModel) { viewState ->
+ renderList(viewState.polls.filterIsInstance(PollSummary.ActivePoll::class.java))
+ }
+
+ private fun renderList(polls: List) {
+ roomActivePollsController.setData(polls)
+ views.roomPollsEmptyTitle.isVisible = polls.isEmpty()
+ }
+
+ override fun onPollClicked(pollId: String) {
+ // TODO navigate to details
+ Timber.d("poll with id $pollId clicked")
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt
index b6fa997f41..514f2529e9 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt
@@ -25,6 +25,7 @@ import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorPreferenceCategory
import im.vector.app.core.preference.VectorSwitchPreference
+import im.vector.app.core.utils.copyToClipboard
import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.home.NightlyProxy
import im.vector.app.features.rageshake.RageShake
@@ -64,6 +65,14 @@ class VectorSettingsAdvancedSettingsFragment :
override fun bindPref() {
setupRageShakeSection()
setupNightlySection()
+ setupDevToolsSection()
+ }
+
+ private fun setupDevToolsSection() {
+ findPreference("SETTINGS_ACCESS_TOKEN")?.setOnPreferenceClickListener {
+ copyToClipboard(requireActivity(), session.sessionParams.credentials.accessToken)
+ true
+ }
}
private fun setupRageShakeSection() {
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index d56f4ad715..9cb894bb58 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -419,7 +419,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
// Next media player is already attached to this player and will start playing automatically
if (nextMediaPlayer != null) return
- val hasEnded = !isLiveListening && mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence
+ val currentSequence = playlist.currentSequence ?: 0
+ val lastChunkSequence = mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence ?: 0
+ val hasEnded = !isLiveListening && currentSequence >= lastChunkSequence
if (hasEnded) {
// We'll not receive new chunks anymore so we can stop the live listening
stop()
diff --git a/vector/src/main/res/layout/fragment_room_polls.xml b/vector/src/main/res/layout/fragment_room_polls.xml
new file mode 100644
index 0000000000..396d6fd8c5
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_room_polls.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_room_polls_list.xml b/vector/src/main/res/layout/fragment_room_polls_list.xml
new file mode 100644
index 0000000000..8eb27e5e00
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_room_polls_list.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/item_poll.xml b/vector/src/main/res/layout/item_poll.xml
new file mode 100644
index 0000000000..956ecf9b3c
--- /dev/null
+++ b/vector/src/main/res/layout/item_poll.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml
index 9260b33162..6399d54cbb 100644
--- a/vector/src/main/res/xml/vector_settings_advanced_settings.xml
+++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml
@@ -93,6 +93,12 @@
android:title="@string/settings_key_requests"
app:fragment="im.vector.app.features.settings.devtools.KeyRequestsFragment" />
+
+
()
+ private val initialState = RoomPollsViewState(ROOM_ID)
+
+ private fun createViewModel(): RoomPollsViewModel {
+ return RoomPollsViewModel(
+ initialState = initialState,
+ getPollsUseCase = fakeGetPollsUseCase,
+ )
+ }
+
+ @Test
+ fun `given SetFilter action when handle then useCase is called with given filter and viewState is updated`() {
+ // Given
+ val filter = RoomPollsFilterType.ACTIVE
+ val action = RoomPollsAction.SetFilter(filter = filter)
+ val polls = listOf(givenAPollSummary())
+ every { fakeGetPollsUseCase.execute(any()) } returns flowOf(polls)
+ val viewModel = createViewModel()
+ val expectedViewState = initialState.copy(polls = polls)
+
+ // When
+ val viewModelTest = viewModel.test()
+ viewModel.pollsCollectionJob = null
+ viewModel.handle(action)
+
+ // Then
+ viewModelTest
+ .assertLatestState(expectedViewState)
+ .finish()
+ viewModel.pollsCollectionJob.shouldNotBeNull()
+ verify {
+ viewModel.pollsCollectionJob?.cancel()
+ fakeGetPollsUseCase.execute(filter)
+ }
+ }
+
+ private fun givenAPollSummary(): PollSummary {
+ return mockk()
+ }
+}