Merge remote-tracking branch 'origin/feature/ons/render_ended_poll' into feature/ons/render_ended_poll

This commit is contained in:
Onuray Sahin 2023-01-06 18:40:27 +03:00
commit d62a9f69ae
44 changed files with 1196 additions and 196 deletions

View file

@ -29,7 +29,7 @@ buildscript {
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730' 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.google.android.gms:oss-licenses-plugin:0.10.5'
classpath "com.likethesalad.android:stem-plugin:2.2.3" 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.dokka:dokka-gradle-plugin:1.7.20"
classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3' classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'

1
changelog.d/7864.wip Normal file
View file

@ -0,0 +1 @@
[Poll] Render active polls list of a room

1
changelog.d/7879.bugfix Normal file
View file

@ -0,0 +1 @@
Reduce number of crypto database transactions when handling the sync response

1
changelog.d/7899.bugfix Normal file
View file

@ -0,0 +1 @@
[Voice Broadcast] Stop listening if we reach the last received chunk and there is no last sequence number

View file

@ -2335,6 +2335,7 @@
<item quantity="one">"One person"</item> <item quantity="one">"One person"</item>
<item quantity="other">"%1$d people"</item> <item quantity="other">"%1$d people"</item>
</plurals> </plurals>
<string name="room_profile_section_more_polls">Poll history</string>
<string name="room_profile_section_more_uploads">Uploads</string> <string name="room_profile_section_more_uploads">Uploads</string>
<string name="room_profile_section_more_leave">Leave Room</string> <string name="room_profile_section_more_leave">Leave Room</string>
<string name="direct_room_profile_section_more_leave">Leave</string> <string name="direct_room_profile_section_more_leave">Leave</string>
@ -3192,6 +3193,8 @@
<string name="closed_poll_option_title">Closed poll</string> <string name="closed_poll_option_title">Closed poll</string>
<string name="closed_poll_option_description">Results are only revealed when you end the poll</string> <string name="closed_poll_option_description">Results are only revealed when you end the poll</string>
<string name="ended_poll_indicator">Ended the poll.</string> <string name="ended_poll_indicator">Ended the poll.</string>
<string name="room_polls_active">Active polls</string>
<string name="room_polls_active_no_item">There are no active polls in this room</string>
<!-- Location --> <!-- Location -->
<string name="location_activity_title_static_sharing">Share location</string> <string name="location_activity_title_static_sharing">Share location</string>
@ -3507,4 +3510,7 @@
<string name="message_reply_to_sender_created_poll">created a poll.</string> <string name="message_reply_to_sender_created_poll">created a poll.</string>
<string name="message_reply_to_sender_ended_poll">ended a poll.</string> <string name="message_reply_to_sender_ended_poll">ended a poll.</string>
<string name="message_reply_to_ended_poll_preview">Ended poll</string> <string name="message_reply_to_ended_poll_preview">Ended poll</string>
<string name="settings_access_token">Access Token</string>
<string name="settings_access_token_summary">Your access token gives full access to your account. Do not share it with anyone.</string>
</resources> </resources>

View file

@ -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?,
)

View file

@ -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.model.toRest
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository 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.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.DeleteDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask 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 isStarting = AtomicBoolean(false)
private val isStarted = AtomicBoolean(false) private val isStarted = AtomicBoolean(false)
fun onStateEvent(roomId: String, event: Event) { fun onStateEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) {
when (event.type) { when (event.type) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(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 // handle state events
if (event.isStateEvent()) { if (event.isStateEvent()) {
when (event.type) { when (event.type) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(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. * A sync response has been received.
* *
* @param syncResponse the syncResponse * @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) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching { runCatching {
if (syncResponse.deviceLists != null) { 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 if (!event.isStateEvent()) return
val eventContent = event.content.toModel<RoomHistoryVisibilityContent>() val eventContent = event.content.toModel<RoomHistoryVisibilityContent>()
val historyVisibility = eventContent?.historyVisibility val historyVisibility = eventContent?.historyVisibility
if (historyVisibility == null) { if (historyVisibility == null) {
cryptoStore.setShouldShareHistory(roomId, false) if (cryptoStoreAggregator != null) {
cryptoStoreAggregator.setShouldShareHistoryData[roomId] = false
} else {
// Store immediately
cryptoStore.setShouldShareHistory(roomId, false)
}
} else { } else {
cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED) if (cryptoStoreAggregator != null) {
cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory()) 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())
}
} }
} }

View file

@ -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.extensions.measureMetric
import org.matrix.android.sdk.api.metrics.DownloadDeviceKeysMetricsPlugin 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.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.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap 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.CryptoInfoMapper
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse 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.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.crypto.tasks.DownloadKeysForUsersTask
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore 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") Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
} }
val userDataToStore = UserDataToStore()
for (userId in filteredUsers) { for (userId in filteredUsers) {
// al devices = // al devices =
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) } val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
@ -404,7 +408,7 @@ internal class DeviceListManager @Inject constructor(
} }
// Update the store // Update the store
// Note that devices which aren't in the response will be removed from the stores // 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 { 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 { val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
} }
cryptoStore.storeUserCrossSigningKeys( userDataToStore.userIdentities[userId] = UserIdentity(
userId, masterKey = masterKey,
masterKey, selfSigningKey = selfSigningKey,
selfSigningKey, userSigningKey = userSigningKey
userSigningKey
) )
} }
cryptoStore.storeData(userDataToStore)
// Update devices trust for these users // Update devices trust for these users
// dispatchDeviceChange(downloadUsers) // dispatchDeviceChange(downloadUsers)

View file

@ -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.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest 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.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.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo 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.keysbackup.SavedKeyBackupKeyInfo
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo 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.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper 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.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.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmOutboundGroupSession import org.matrix.olm.OlmOutboundGroupSession
@ -230,11 +231,12 @@ internal interface IMXCryptoStore {
*/ */
fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?) fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?)
fun storeUserCrossSigningKeys( /**
* Store the cross signing keys for the user userId.
*/
fun storeUserIdentity(
userId: String, userId: String,
masterKey: CryptoCrossSigningKey?, userIdentity: UserIdentity
selfSigningKey: CryptoCrossSigningKey?,
userSigningKey: CryptoCrossSigningKey?
) )
/** /**
@ -290,6 +292,13 @@ internal interface IMXCryptoStore {
fun shouldEncryptForInvitedMembers(roomId: String): Boolean 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 setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean)
fun shouldShareHistory(roomId: String): Boolean fun shouldShareHistory(roomId: String): Boolean
@ -580,4 +589,14 @@ internal interface IMXCryptoStore {
fun areDeviceKeysUploaded(): Boolean fun areDeviceKeysUploaded(): Boolean
fun tidyUpDataBase() fun tidyUpDataBase()
fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest> fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest>
/**
* 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)
} }

View file

@ -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<String, Map<String, CryptoDeviceInfo>> = mutableMapOf(),
/**
* Map of userId -> [UserIdentity].
*/
val userIdentities: MutableMap<String, UserIdentity> = mutableMapOf(),
)

View file

@ -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<String, Boolean> = mutableMapOf(),
val setShouldEncryptForInvitedMembersData: MutableMap<String, Boolean> = mutableMapOf(),
) {
fun isEmpty(): Boolean {
return setShouldShareHistoryData.isEmpty() &&
setShouldEncryptForInvitedMembersData.isEmpty()
}
}

View file

@ -20,10 +20,12 @@ import android.util.Base64
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.RealmObject import io.realm.RealmObject
import timber.log.Timber
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.ObjectOutputStream import java.io.ObjectOutputStream
import java.util.zip.GZIPInputStream import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
import kotlin.system.measureTimeMillis
/** /**
* Get realm, invoke the action, close realm, and return the result of the action. * Get realm, invoke the action, close realm, and return the result of the action.
@ -55,10 +57,12 @@ internal fun <T : RealmObject> doRealmQueryAndCopyList(realmConfiguration: Realm
/** /**
* Get realm instance, invoke the action in a transaction and close realm. * Get realm instance, invoke the action in a transaction and close realm.
*/ */
internal fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { internal fun doRealmTransaction(tag: String, realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
Realm.getInstance(realmConfiguration).use { realm -> measureTimeMillis {
realm.executeTransaction { action.invoke(it) } 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) { internal fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {

View file

@ -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.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest 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.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.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo 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.keysbackup.SavedKeyBackupKeyInfo
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo 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.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper 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.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.CrossSigningKeysMapper
import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
@ -147,7 +148,7 @@ internal class RealmCryptoStore @Inject constructor(
init { init {
// Ensure CryptoMetadataEntity is inserted in DB // Ensure CryptoMetadataEntity is inserted in DB
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("init", realmConfiguration) { realm ->
var currentMetadata = realm.where<CryptoMetadataEntity>().findFirst() var currentMetadata = realm.where<CryptoMetadataEntity>().findFirst()
var deleteAll = false var deleteAll = false
@ -189,7 +190,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun deleteStore() { override fun deleteStore() {
doRealmTransaction(realmConfiguration) { doRealmTransaction("deleteStore", realmConfiguration) {
it.deleteAll() it.deleteAll()
} }
} }
@ -218,7 +219,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun storeDeviceId(deviceId: String) { override fun storeDeviceId(deviceId: String) {
doRealmTransaction(realmConfiguration) { doRealmTransaction("storeDeviceId", realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.deviceId = deviceId it.where<CryptoMetadataEntity>().findFirst()?.deviceId = deviceId
} }
} }
@ -230,7 +231,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun saveOlmAccount() { override fun saveOlmAccount() {
doRealmTransaction(realmConfiguration) { doRealmTransaction("saveOlmAccount", realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.putOlmAccount(olmAccount) it.where<CryptoMetadataEntity>().findFirst()?.putOlmAccount(olmAccount)
} }
} }
@ -248,7 +249,7 @@ internal class RealmCryptoStore @Inject constructor(
@Synchronized @Synchronized
override fun getOrCreateOlmAccount(): OlmAccount { override fun getOrCreateOlmAccount(): OlmAccount {
doRealmTransaction(realmConfiguration) { doRealmTransaction("getOrCreateOlmAccount", realmConfiguration) {
val metaData = it.where<CryptoMetadataEntity>().findFirst() val metaData = it.where<CryptoMetadataEntity>().findFirst()
val existing = metaData!!.getOlmAccount() val existing = metaData!!.getOlmAccount()
if (existing == null) { if (existing == null) {
@ -288,129 +289,139 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?) { override fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("storeUserDevices", realmConfiguration) { realm ->
if (devices == null) { storeUserDevices(realm, userId, devices)
Timber.d("Remove user $userId") }
// Remove the user }
UserEntity.delete(realm, userId)
} else { private fun storeUserDevices(realm: Realm, userId: String, devices: Map<String, CryptoDeviceInfo>?) {
val userEntity = UserEntity.getOrCreate(realm, userId) if (devices == null) {
// First delete the removed devices Timber.d("Remove user $userId")
val deviceIds = devices.keys // Remove the user
userEntity.devices.toTypedArray().iterator().let { UserEntity.delete(realm, userId)
while (it.hasNext()) { } else {
val deviceInfoEntity = it.next() val userEntity = UserEntity.getOrCreate(realm, userId)
if (deviceInfoEntity.deviceId !in deviceIds) { // First delete the removed devices
Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId") val deviceIds = devices.keys
deviceInfoEntity.deleteOnCascade() 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 -> // Then update existing devices or add new one
val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId } devices.values.forEach { cryptoDeviceInfo ->
if (existingDeviceInfoEntity == null) { val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId }
// Add the device if (existingDeviceInfoEntity == null) {
Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId") // Add the device
val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo) Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId")
newEntity.firstTimeSeenLocalTs = clock.epochMillis() val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo)
userEntity.devices.add(newEntity) newEntity.firstTimeSeenLocalTs = clock.epochMillis()
} else { userEntity.devices.add(newEntity)
// Update the device } else {
Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId") // Update the device
CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo) Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId")
} CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo)
} }
} }
} }
} }
override fun storeUserCrossSigningKeys( override fun storeUserIdentity(
userId: String, userId: String,
masterKey: CryptoCrossSigningKey?, userIdentity: UserIdentity,
selfSigningKey: CryptoCrossSigningKey?,
userSigningKey: CryptoCrossSigningKey?
) { ) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("storeUserIdentity", realmConfiguration) { realm ->
UserEntity.getOrCreate(realm, userId) storeUserIdentity(realm, userId, userIdentity)
.let { userEntity -> }
if (masterKey == null || selfSigningKey == null) { }
// The user has disabled cross signing?
userEntity.crossSigningInfoEntity?.deleteOnCascade() private fun storeUserIdentity(
userEntity.crossSigningInfoEntity = null realm: Realm,
} else { userId: String,
var shouldResetMyDevicesLocalTrust = false userIdentity: UserIdentity,
CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo -> ) {
// What should we do if we detect a change of the keys? UserEntity.getOrCreate(realm, userId)
val existingMaster = signingInfo.getMasterKey() .let { userEntity ->
if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) { if (userIdentity.masterKey == null || userIdentity.selfSigningKey == null) {
crossSigningKeysMapper.update(existingMaster, masterKey) // The user has disabled cross signing?
} else { userEntity.crossSigningInfoEntity?.deleteOnCascade()
Timber.d("## CrossSigning MSK change for $userId") userEntity.crossSigningInfoEntity = null
val keyEntity = crossSigningKeysMapper.map(masterKey) } else {
signingInfo.setMasterKey(keyEntity) var shouldResetMyDevicesLocalTrust = false
if (userId == this.userId) { CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo ->
shouldResetMyDevicesLocalTrust = true // What should we do if we detect a change of the keys?
// my msk has changed! clear my private key val existingMaster = signingInfo.getMasterKey()
// Could we have some race here? e.g I am the one that did change the keys if (existingMaster != null && existingMaster.publicKeyBase64 == userIdentity.masterKey.unpaddedBase64PublicKey) {
// could i get this update to early and clear the private keys? crossSigningKeysMapper.update(existingMaster, userIdentity.masterKey)
// -> initializeCrossSigning is guarding for that by storing all at once } else {
realm.where<CryptoMetadataEntity>().findFirst()?.apply { Timber.d("## CrossSigning MSK change for $userId")
xSignMasterPrivateKey = null 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<CryptoMetadataEntity>().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<CryptoMetadataEntity>().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<CryptoMetadataEntity>().findFirst()?.apply {
xSignUserPrivateKey = null
}
}
}
}
// When my cross signing keys are reset, we consider clearing all existing device trust
if (shouldResetMyDevicesLocalTrust) {
realm.where<UserEntity>()
.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<CryptoMetadataEntity>().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<CryptoMetadataEntity>().findFirst()?.apply {
xSignUserPrivateKey = null
}
}
}
}
// When my cross signing keys are reset, we consider clearing all existing device trust
if (shouldResetMyDevicesLocalTrust) {
realm.where<UserEntity>()
.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? { override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
@ -480,7 +491,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}") Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}")
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("storePrivateKeysInfo", realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply { realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignMasterPrivateKey = msk xSignMasterPrivateKey = msk
xSignUserPrivateKey = usk xSignUserPrivateKey = usk
@ -490,7 +501,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) { override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("saveBackupRecoveryKey", realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply { realm.where<CryptoMetadataEntity>().findFirst()?.apply {
keyBackupRecoveryKey = recoveryKey keyBackupRecoveryKey = recoveryKey
keyBackupRecoveryKeyVersion = version keyBackupRecoveryKeyVersion = version
@ -516,7 +527,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun storeMSKPrivateKey(msk: String?) { override fun storeMSKPrivateKey(msk: String?) {
Timber.v("## CRYPTO | *** storeMSKPrivateKey ${msk != null} ") Timber.v("## CRYPTO | *** storeMSKPrivateKey ${msk != null} ")
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("storeMSKPrivateKey", realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply { realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignMasterPrivateKey = msk xSignMasterPrivateKey = msk
} }
@ -525,7 +536,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun storeSSKPrivateKey(ssk: String?) { override fun storeSSKPrivateKey(ssk: String?) {
Timber.v("## CRYPTO | *** storeSSKPrivateKey ${ssk != null} ") Timber.v("## CRYPTO | *** storeSSKPrivateKey ${ssk != null} ")
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("storeSSKPrivateKey", realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply { realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignSelfSignedPrivateKey = ssk xSignSelfSignedPrivateKey = ssk
} }
@ -534,7 +545,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun storeUSKPrivateKey(usk: String?) { override fun storeUSKPrivateKey(usk: String?) {
Timber.v("## CRYPTO | *** storeUSKPrivateKey ${usk != null} ") Timber.v("## CRYPTO | *** storeUSKPrivateKey ${usk != null} ")
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("storeUSKPrivateKey", realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply { realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignUserPrivateKey = usk xSignUserPrivateKey = usk
} }
@ -667,7 +678,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun storeRoomAlgorithm(roomId: String, algorithm: String?) { override fun storeRoomAlgorithm(roomId: String, algorithm: String?) {
doRealmTransaction(realmConfiguration) { doRealmTransaction("storeRoomAlgorithm", realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).let { entity -> CryptoRoomEntity.getOrCreate(it, roomId).let { entity ->
entity.algorithm = algorithm entity.algorithm = algorithm
// store anyway the new algorithm, but mark the room // 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) { override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) {
doRealmTransaction(realmConfiguration) { doRealmTransaction("setShouldEncryptForInvitedMembers", realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers
} }
} }
@ -716,7 +727,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) { override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) {
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.v("setShouldShareHistory for room $roomId is $shouldShareHistory") .v("setShouldShareHistory for room $roomId is $shouldShareHistory")
doRealmTransaction(realmConfiguration) { doRealmTransaction("setShouldShareHistory", realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory
} }
} }
@ -733,7 +744,7 @@ internal class RealmCryptoStore @Inject constructor(
if (sessionIdentifier != null) { if (sessionIdentifier != null) {
val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey) val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey)
doRealmTransaction(realmConfiguration) { doRealmTransaction("storeSession", realmConfiguration) {
val realmOlmSession = OlmSessionEntity().apply { val realmOlmSession = OlmSessionEntity().apply {
primaryKey = key primaryKey = key
sessionId = sessionIdentifier sessionId = sessionIdentifier
@ -790,7 +801,7 @@ internal class RealmCryptoStore @Inject constructor(
return return
} }
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("storeInboundGroupSessions", realmConfiguration) { realm ->
sessions.forEach { wrapper -> sessions.forEach { wrapper ->
val sessionIdentifier = try { val sessionIdentifier = try {
@ -914,7 +925,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun removeInboundGroupSession(sessionId: String, senderKey: String) { override fun removeInboundGroupSession(sessionId: String, senderKey: String) {
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
doRealmTransaction(realmConfiguration) { doRealmTransaction("removeInboundGroupSession", realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>() it.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findAll() .findAll()
@ -933,7 +944,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun setKeyBackupVersion(keyBackupVersion: String?) { override fun setKeyBackupVersion(keyBackupVersion: String?) {
doRealmTransaction(realmConfiguration) { doRealmTransaction("setKeyBackupVersion", realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.backupVersion = keyBackupVersion it.where<CryptoMetadataEntity>().findFirst()?.backupVersion = keyBackupVersion
} }
} }
@ -945,7 +956,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) { override fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) {
doRealmTransaction(realmConfiguration) { doRealmTransaction("setKeysBackupData", realmConfiguration) {
if (keysBackupData == null) { if (keysBackupData == null) {
// Clear the table // Clear the table
it.where<KeysBackupDataEntity>() it.where<KeysBackupDataEntity>()
@ -959,7 +970,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun resetBackupMarkers() { override fun resetBackupMarkers() {
doRealmTransaction(realmConfiguration) { doRealmTransaction("resetBackupMarkers", realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>() it.where<OlmInboundGroupSessionEntity>()
.findAll() .findAll()
.map { inboundGroupSession -> .map { inboundGroupSession ->
@ -973,7 +984,7 @@ internal class RealmCryptoStore @Inject constructor(
return return
} }
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("markBackupDoneForInboundGroupSessions", realmConfiguration) { realm ->
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
try { try {
val sessionIdentifier = val sessionIdentifier =
@ -1032,13 +1043,13 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) {
doRealmTransaction(realmConfiguration) { doRealmTransaction("setGlobalBlacklistUnverifiedDevices", realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices = block it.where<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices = block
} }
} }
override fun enableKeyGossiping(enable: Boolean) { override fun enableKeyGossiping(enable: Boolean) {
doRealmTransaction(realmConfiguration) { doRealmTransaction("enableKeyGossiping", realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.globalEnableKeyGossiping = enable it.where<CryptoMetadataEntity>().findFirst()?.globalEnableKeyGossiping = enable
} }
} }
@ -1062,13 +1073,13 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun enableShareKeyOnInvite(enable: Boolean) { override fun enableShareKeyOnInvite(enable: Boolean) {
doRealmTransaction(realmConfiguration) { doRealmTransaction("enableShareKeyOnInvite", realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.enableKeyForwardingOnInvite = enable it.where<CryptoMetadataEntity>().findFirst()?.enableKeyForwardingOnInvite = enable
} }
} }
override fun setDeviceKeysUploaded(uploaded: Boolean) { override fun setDeviceKeysUploaded(uploaded: Boolean) {
doRealmTransaction(realmConfiguration) { doRealmTransaction("setDeviceKeysUploaded", realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer = uploaded it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer = uploaded
} }
} }
@ -1115,7 +1126,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) { override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("blockUnverifiedDevicesInRoom", realmConfiguration) { realm ->
CryptoRoomEntity.getById(realm, roomId) CryptoRoomEntity.getById(realm, roomId)
?.blacklistUnverifiedDevices = block ?.blacklistUnverifiedDevices = block
} }
@ -1135,7 +1146,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map<String, Int>) { override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map<String, Int>) {
doRealmTransaction(realmConfiguration) { doRealmTransaction("saveDeviceTrackingStatuses", realmConfiguration) {
deviceTrackingStatuses deviceTrackingStatuses
.map { entry -> .map { entry ->
UserEntity.getOrCreate(it, entry.key) UserEntity.getOrCreate(it, entry.key)
@ -1268,7 +1279,7 @@ internal class RealmCryptoStore @Inject constructor(
): OutgoingKeyRequest { ): OutgoingKeyRequest {
// Insert the request and return the one passed in parameter // Insert the request and return the one passed in parameter
lateinit var request: OutgoingKeyRequest lateinit var request: OutgoingKeyRequest
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("getOrAddOutgoingRoomKeyRequest", realmConfiguration) { realm ->
val existing = realm.where<OutgoingKeyRequestEntity>() val existing = realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId)
@ -1306,7 +1317,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) { override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("updateOutgoingRoomKeyRequestState", realmConfiguration) { realm ->
realm.where<OutgoingKeyRequestEntity>() realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.apply { .findFirst()?.apply {
@ -1320,7 +1331,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) { override fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("updateOutgoingRoomKeyRequiredIndex", realmConfiguration) { realm ->
realm.where<OutgoingKeyRequestEntity>() realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.apply { .findFirst()?.apply {
@ -1337,7 +1348,7 @@ internal class RealmCryptoStore @Inject constructor(
fromDevice: String?, fromDevice: String?,
event: Event event: Event
) { ) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("updateOutgoingRoomKeyReply", realmConfiguration) { realm ->
realm.where<OutgoingKeyRequestEntity>() realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId)
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId)
@ -1353,7 +1364,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun deleteOutgoingRoomKeyRequest(requestId: String) { override fun deleteOutgoingRoomKeyRequest(requestId: String) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("deleteOutgoingRoomKeyRequest", realmConfiguration) { realm ->
realm.where<OutgoingKeyRequestEntity>() realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.deleteOnCascade() .findFirst()?.deleteOnCascade()
@ -1361,7 +1372,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) { override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("deleteOutgoingRoomKeyRequestInState", realmConfiguration) { realm ->
realm.where<OutgoingKeyRequestEntity>() realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, state.name) .equalTo(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, state.name)
.findAll() .findAll()
@ -1497,7 +1508,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) { override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("setMyCrossSigningInfo", realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.userId?.let { userId -> realm.where<CryptoMetadataEntity>().findFirst()?.userId?.let { userId ->
addOrUpdateCrossSigningInfo(realm, userId, info) addOrUpdateCrossSigningInfo(realm, userId, info)
} }
@ -1505,7 +1516,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) { override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("setUserKeysAsTrusted", realmConfiguration) { realm ->
val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId) .equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst() .findFirst()
@ -1525,7 +1536,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) { override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("setDeviceTrust", realmConfiguration) { realm ->
realm.where(DeviceInfoEntity::class.java) realm.where(DeviceInfoEntity::class.java)
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
.findFirst()?.let { deviceInfoEntity -> .findFirst()?.let { deviceInfoEntity ->
@ -1545,7 +1556,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun clearOtherUserTrust() { override fun clearOtherUserTrust() {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("clearOtherUserTrust", realmConfiguration) { realm ->
val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java)
.findAll() .findAll()
xInfoEntities?.forEach { info -> xInfoEntities?.forEach { info ->
@ -1560,7 +1571,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun updateUsersTrust(check: (String) -> Boolean) { override fun updateUsersTrust(check: (String) -> Boolean) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("updateUsersTrust", realmConfiguration) { realm ->
val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java)
.findAll() .findAll()
xInfoEntities?.forEach { xInfoEntity -> xInfoEntities?.forEach { xInfoEntity ->
@ -1668,13 +1679,13 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) { override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("setCrossSigningInfo", realmConfiguration) { realm ->
addOrUpdateCrossSigningInfo(realm, userId, info) addOrUpdateCrossSigningInfo(realm, userId, info)
} }
} }
override fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) { override fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("markMyMasterKeyAsLocallyTrusted", realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.userId?.let { myUserId -> realm.where<CryptoMetadataEntity>().findFirst()?.userId?.let { myUserId ->
CrossSigningInfoEntity.get(realm, myUserId)?.getMasterKey()?.let { xInfoEntity -> CrossSigningInfoEntity.get(realm, myUserId)?.getMasterKey()?.let { xInfoEntity ->
val level = xInfoEntity.trustLevelEntity val level = xInfoEntity.trustLevelEntity
@ -1713,7 +1724,7 @@ internal class RealmCryptoStore @Inject constructor(
val roomId = withHeldContent.roomId ?: return val roomId = withHeldContent.roomId ?: return
val sessionId = withHeldContent.sessionId ?: return val sessionId = withHeldContent.sessionId ?: return
if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("addWithHeldMegolmSession", realmConfiguration) { realm ->
WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let { WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let {
it.code = withHeldContent.code it.code = withHeldContent.code
it.senderKey = withHeldContent.senderKey it.senderKey = withHeldContent.senderKey
@ -1745,7 +1756,7 @@ internal class RealmCryptoStore @Inject constructor(
deviceIdentityKey: String, deviceIdentityKey: String,
chainIndex: Int chainIndex: Int
) { ) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("markedSessionAsShared", realmConfiguration) { realm ->
SharedSessionEntity.create( SharedSessionEntity.create(
realm = realm, realm = realm,
roomId = roomId, roomId = roomId,
@ -1794,7 +1805,7 @@ internal class RealmCryptoStore @Inject constructor(
*/ */
override fun tidyUpDataBase() { override fun tidyUpDataBase() {
val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000 val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction("tidyUpDataBase", realmConfiguration) { realm ->
// Clean the old ones? // Clean the old ones?
realm.where<OutgoingKeyRequestEntity>() realm.where<OutgoingKeyRequestEntity>()
@ -1815,4 +1826,31 @@ internal class RealmCryptoStore @Inject constructor(
// Can we do something for WithHeldSessionEntity? // 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)
}
}
}
} }

View file

@ -42,14 +42,12 @@ internal class StreamEventsManager @Inject constructor() {
listeners.remove(listener) listeners.remove(listener)
} }
fun dispatchLiveEventReceived(event: Event, roomId: String, initialSync: Boolean) { fun dispatchLiveEventReceived(event: Event, roomId: String) {
Timber.v("## dispatchLiveEventReceived ${event.eventId}") Timber.v("## dispatchLiveEventReceived ${event.eventId}")
coroutineScope.launch { coroutineScope.launch {
if (!initialSync) { listeners.forEach {
listeners.forEach { tryOrNull {
tryOrNull { it.onLiveEvent(roomId, event)
it.onLiveEvent(roomId, event)
}
} }
} }
} }

View file

@ -176,7 +176,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
} }
// Give info to crypto module // Give info to crypto module
cryptoService.onStateEvent(roomId, event) cryptoService.onStateEvent(roomId, event, null)
} }
roomMemberContentsByUser.getOrPut(event.senderId) { roomMemberContentsByUser.getOrPut(event.senderId) {

View file

@ -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.api.session.sync.model.SyncResponse
import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService 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.SessionDatabase
import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.session.SessionListeners import org.matrix.android.sdk.internal.session.SessionListeners
@ -92,7 +93,7 @@ internal class SyncResponseHandler @Inject constructor(
postTreatmentSyncResponse(syncResponse, isInitialSync) postTreatmentSyncResponse(syncResponse, isInitialSync)
markCryptoSyncCompleted(syncResponse) markCryptoSyncCompleted(syncResponse, aggregator.cryptoStoreAggregator)
handlePostSync() 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") { relevantPlugins.measureSpan("task", "crypto_sync_handler_onSyncCompleted") {
measureTimeMillis { measureTimeMillis {
cryptoSyncHandler.onSyncCompleted(syncResponse) cryptoSyncHandler.onSyncCompleted(syncResponse, cryptoStoreAggregator)
}.also { }.also {
Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms") Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms")
} }

View file

@ -16,6 +16,8 @@
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync
import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator
internal class SyncResponsePostTreatmentAggregator { internal class SyncResponsePostTreatmentAggregator {
// List of RoomId // List of RoomId
val ephemeralFilesToDelete = mutableListOf<String>() val ephemeralFilesToDelete = mutableListOf<String>()
@ -28,4 +30,7 @@ internal class SyncResponsePostTreatmentAggregator {
// Set of users to call `crossSigningService.checkTrustAndAffectedRoomShields` once per sync // Set of users to call `crossSigningService.checkTrustAndAffectedRoomShields` once per sync
val userIdsForCheckingTrustAndAffectedRoomShields = mutableSetOf<String>() val userIdsForCheckingTrustAndAffectedRoomShields = mutableSetOf<String>()
// For the crypto store
val cryptoStoreAggregator = CryptoStoreAggregator()
} }

View file

@ -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.SyncResponse
import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse 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.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.tasks.toDeviceTracingId
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
import org.matrix.android.sdk.internal.session.sync.ProgressReporter import org.matrix.android.sdk.internal.session.sync.ProgressReporter
@ -85,8 +86,8 @@ internal class CryptoSyncHandler @Inject constructor(
} }
} }
fun onSyncCompleted(syncResponse: SyncResponse) { fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) {
cryptoService.onSyncCompleted(syncResponse) cryptoService.onSyncCompleted(syncResponse, cryptoStoreAggregator)
} }
/** /**

View file

@ -258,7 +258,7 @@ internal class RoomSyncHandler @Inject constructor(
root = eventEntity root = eventEntity
} }
// Give info to crypto module // Give info to crypto module
cryptoService.onStateEvent(roomId, event) cryptoService.onStateEvent(roomId, event, aggregator.cryptoStoreAggregator)
roomMemberEventHandler.handle(realm, roomId, event, isInitialSync, aggregator) 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) } roomEntity.chunks.clearWith { it.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) }
roomTypingUsersHandler.handle(realm, roomId, null) roomTypingUsersHandler.handle(realm, roomId, null)
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE) roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE)
roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSummaryUpdater.update(
roomSync.unreadNotifications, roomSync.unreadThreadNotifications, aggregator = aggregator) realm,
roomId,
membership,
roomSync.summary,
roomSync.unreadNotifications,
roomSync.unreadThreadNotifications,
aggregator = aggregator,
)
return roomEntity return roomEntity
} }
@ -423,7 +430,9 @@ internal class RoomSyncHandler @Inject constructor(
val isInitialSync = insertType == EventInsertType.INITIAL_SYNC val isInitialSync = insertType == EventInsertType.INITIAL_SYNC
eventIds.add(event.eventId) eventIds.add(event.eventId)
liveEventService.get().dispatchLiveEventReceived(event, roomId, isInitialSync) if (!isInitialSync) {
liveEventService.get().dispatchLiveEventReceived(event, roomId)
}
if (event.isEncrypted() && !isInitialSync) { if (event.isEncrypted() && !isInitialSync) {
try { try {
@ -486,7 +495,7 @@ internal class RoomSyncHandler @Inject constructor(
} }
} }
// Give info to crypto module // Give info to crypto module
cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync) cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync, aggregator.cryptoStoreAggregator)
// Try to remove local echo // Try to remove local echo
event.unsignedData?.transactionId?.also { txId -> event.unsignedData?.transactionId?.also { txId ->

View file

@ -308,7 +308,7 @@ dependencies {
// Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868 // 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) // 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. //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.testCore
androidTestImplementation libs.androidx.testRunner androidTestImplementation libs.androidx.testRunner

View file

@ -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.members.RoomMemberListViewModel
import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsViewModel import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsViewModel
import im.vector.app.features.roomprofile.permissions.RoomPermissionsViewModel 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.RoomSettingsViewModel
import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel
import im.vector.app.features.roomprofile.uploads.RoomUploadsViewModel import im.vector.app.features.roomprofile.uploads.RoomUploadsViewModel
@ -697,4 +698,9 @@ interface MavericksViewModelModule {
@IntoMap @IntoMap
@MavericksViewModelKey(SetLinkViewModel::class) @MavericksViewModelKey(SetLinkViewModel::class)
fun setLinkViewModelFactory(factory: SetLinkViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun setLinkViewModelFactory(factory: SetLinkViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(RoomPollsViewModel::class)
fun roomPollsViewModelFactory(factory: RoomPollsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
} }

View file

@ -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.members.RoomMemberListFragment
import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsFragment import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsFragment
import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment 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.settings.RoomSettingsFragment
import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment
import im.vector.lib.core.utils.compat.getParcelableCompat import im.vector.lib.core.utils.compat.getParcelableCompat
@ -98,6 +99,7 @@ class RoomProfileActivity :
RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings() RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings()
RoomProfileSharedAction.OpenRoomAliasesSettings -> openRoomAlias() RoomProfileSharedAction.OpenRoomAliasesSettings -> openRoomAlias()
RoomProfileSharedAction.OpenRoomPermissionsSettings -> openRoomPermissions() RoomProfileSharedAction.OpenRoomPermissionsSettings -> openRoomPermissions()
RoomProfileSharedAction.OpenRoomPolls -> openRoomPolls()
RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads() RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads()
RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers() RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers()
RoomProfileSharedAction.OpenRoomNotificationSettings -> openRoomNotificationSettings() RoomProfileSharedAction.OpenRoomNotificationSettings -> openRoomNotificationSettings()
@ -126,6 +128,10 @@ class RoomProfileActivity :
finish() finish()
} }
private fun openRoomPolls() {
addFragmentToBackstack(views.simpleFragmentContainer, RoomPollsFragment::class.java, roomProfileArgs)
}
private fun openRoomUploads() { private fun openRoomUploads() {
addFragmentToBackstack(views.simpleFragmentContainer, RoomUploadsFragment::class.java, roomProfileArgs) addFragmentToBackstack(views.simpleFragmentContainer, RoomUploadsFragment::class.java, roomProfileArgs)
} }

View file

@ -18,6 +18,7 @@
package im.vector.app.features.roomprofile package im.vector.app.features.roomprofile
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.expandableTextItem import im.vector.app.core.epoxy.expandableTextItem
import im.vector.app.core.epoxy.profiles.buildProfileAction import im.vector.app.core.epoxy.profiles.buildProfileAction
@ -56,6 +57,7 @@ class RoomProfileController @Inject constructor(
fun onMemberListClicked() fun onMemberListClicked()
fun onBannedMemberListClicked() fun onBannedMemberListClicked()
fun onNotificationsClicked() fun onNotificationsClicked()
fun onPollHistoryClicked()
fun onUploadsClicked() fun onUploadsClicked()
fun createShortcut() fun createShortcut()
fun onSettingsClicked() fun onSettingsClicked()
@ -263,6 +265,15 @@ class RoomProfileController @Inject constructor(
action = { callback?.onBannedMemberListClicked() } 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( buildProfileAction(
id = "uploads", id = "uploads",
title = stringProvider.getString(R.string.room_profile_section_more_uploads), title = stringProvider.getString(R.string.room_profile_section_more_uploads),

View file

@ -269,6 +269,10 @@ class RoomProfileFragment :
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomNotificationSettings) roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomNotificationSettings)
} }
override fun onPollHistoryClicked() {
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomPolls)
}
override fun onUploadsClicked() { override fun onUploadsClicked() {
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomUploads) roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomUploads)
} }

View file

@ -25,6 +25,7 @@ sealed class RoomProfileSharedAction : VectorSharedAction {
object OpenRoomSettings : RoomProfileSharedAction() object OpenRoomSettings : RoomProfileSharedAction()
object OpenRoomAliasesSettings : RoomProfileSharedAction() object OpenRoomAliasesSettings : RoomProfileSharedAction()
object OpenRoomPermissionsSettings : RoomProfileSharedAction() object OpenRoomPermissionsSettings : RoomProfileSharedAction()
object OpenRoomPolls : RoomProfileSharedAction()
object OpenRoomUploads : RoomProfileSharedAction() object OpenRoomUploads : RoomProfileSharedAction()
object OpenRoomMembers : RoomProfileSharedAction() object OpenRoomMembers : RoomProfileSharedAction()
object OpenBannedRoomMembers : RoomProfileSharedAction() object OpenBannedRoomMembers : RoomProfileSharedAction()

View file

@ -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<List<PollSummary>> {
// 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<List<PollSummary.ActivePoll>> {
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?"
),
)
)
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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,
}

View file

@ -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<FragmentRoomPollsBinding>() {
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() }
}
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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<RoomPollsViewState, RoomPollsAction, RoomPollsViewEvent>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<RoomPollsViewModel, RoomPollsViewState> {
override fun create(initialState: RoomPollsViewState): RoomPollsViewModel
}
companion object : MavericksViewModelFactory<RoomPollsViewModel, RoomPollsViewState> 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)
}
}

View file

@ -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<PollSummary> = emptyList(),
) : MavericksState {
constructor(roomProfileArgs: RoomProfileArgs) : this(roomId = roomProfileArgs.roomId)
}

View file

@ -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<ActivePollItem.Holder>(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<TextView>(R.id.pollActiveDate)
val title by bind<TextView>(R.id.pollActiveTitle)
}
}

View file

@ -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<List<PollSummary.ActivePoll>>() {
interface Listener {
fun onPollClicked(pollId: String)
}
var listener: Listener? = null
override fun buildModels(data: List<PollSummary.ActivePoll>?) {
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)
}
}
}
}
}

View file

@ -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<FragmentRoomPollsListBinding>(),
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<PollSummary.ActivePoll>) {
roomActivePollsController.setData(polls)
views.roomPollsEmptyTitle.isVisible = polls.isEmpty()
}
override fun onPollClicked(pollId: String) {
// TODO navigate to details
Timber.d("poll with id $pollId clicked")
}
}

View file

@ -25,6 +25,7 @@ import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorPreferenceCategory import im.vector.app.core.preference.VectorPreferenceCategory
import im.vector.app.core.preference.VectorSwitchPreference 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.analytics.plan.MobileScreen
import im.vector.app.features.home.NightlyProxy import im.vector.app.features.home.NightlyProxy
import im.vector.app.features.rageshake.RageShake import im.vector.app.features.rageshake.RageShake
@ -64,6 +65,14 @@ class VectorSettingsAdvancedSettingsFragment :
override fun bindPref() { override fun bindPref() {
setupRageShakeSection() setupRageShakeSection()
setupNightlySection() setupNightlySection()
setupDevToolsSection()
}
private fun setupDevToolsSection() {
findPreference<VectorPreference>("SETTINGS_ACCESS_TOKEN")?.setOnPreferenceClickListener {
copyToClipboard(requireActivity(), session.sessionParams.credentials.accessToken)
true
}
} }
private fun setupRageShakeSection() { private fun setupRageShakeSection() {

View file

@ -419,7 +419,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
// Next media player is already attached to this player and will start playing automatically // Next media player is already attached to this player and will start playing automatically
if (nextMediaPlayer != null) return 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) { if (hasEnded) {
// We'll not receive new chunks anymore so we can stop the live listening // We'll not receive new chunks anymore so we can stop the live listening
stop() stop()

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/roomPollsToolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:title="@string/room_profile_section_more_polls" />
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/roomPollsTabs"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4dp"
android:layout_marginTop="20dp"
android:background="?android:colorBackground"
app:layout_constraintBottom_toTopOf="@id/roomPollsViewPager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout"
app:tabGravity="start"
app:tabIndicatorFullWidth="false"
app:tabIndicatorHeight="1dp"
app:tabMaxWidth="0dp"
app:tabMode="scrollable"
app:tabPaddingBottom="-15dp"
app:tabSelectedTextColor="?colorSecondary"
app:tabTextAppearance="@style/TextAppearance.Vector.Body"
app:tabTextColor="?vctr_content_primary" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/roomPollsViewPager"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomPollsTabs" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/roomPollsList"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="5"
tools:listitem="@layout/item_poll" />
<TextView
android:id="@+id/roomPollsEmptyTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin"
android:gravity="center"
android:textAppearance="@style/TextAppearance.Vector.Body"
android:textColor="?vctr_content_secondary"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/roomPollsEmptyGuideline"
tools:text="@string/room_polls_active_no_item" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/roomPollsEmptyGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.33" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground">
<TextView
android:id="@+id/pollActiveDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:textAppearance="@style/TextAppearance.Vector.Caption"
android:textColor="?vctr_content_tertiary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="28/06/22" />
<ImageView
android:id="@+id/pollActiveIcon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginTop="12dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_attachment_poll"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pollActiveDate"
app:tint="?vctr_content_secondary"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/pollActiveTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="9dp"
android:textAppearance="@style/TextAppearance.Vector.Subtitle"
android:textColor="?vctr_content_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/pollActiveIcon"
app:layout_constraintTop_toBottomOf="@id/pollActiveDate"
tools:text="Which sport should the pupils do this year?" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -93,6 +93,12 @@
android:title="@string/settings_key_requests" android:title="@string/settings_key_requests"
app:fragment="im.vector.app.features.settings.devtools.KeyRequestsFragment" /> app:fragment="im.vector.app.features.settings.devtools.KeyRequestsFragment" />
<im.vector.app.core.preference.VectorPreference
android:key="SETTINGS_ACCESS_TOKEN"
android:persistent="false"
android:summary="@string/settings_access_token_summary"
android:title="@string/settings_access_token" />
</im.vector.app.core.preference.VectorPreferenceCategory> </im.vector.app.core.preference.VectorPreferenceCategory>
<im.vector.app.core.preference.VectorPreferenceCategory <im.vector.app.core.preference.VectorPreferenceCategory

View file

@ -0,0 +1,76 @@
/*
* 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.test.MavericksTestRule
import im.vector.app.test.test
import im.vector.app.test.testDispatcher
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import org.amshove.kluent.shouldNotBeNull
import org.junit.Rule
import org.junit.Test
private const val ROOM_ID = "room-id"
class RoomPollsViewModelTest {
@get:Rule
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
private val fakeGetPollsUseCase = mockk<GetPollsUseCase>()
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()
}
}