mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-25 10:55:55 +03:00
Merge remote-tracking branch 'origin/feature/ons/render_ended_poll' into feature/ons/render_ended_poll
This commit is contained in:
commit
d62a9f69ae
44 changed files with 1196 additions and 196 deletions
|
@ -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
1
changelog.d/7864.wip
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[Poll] Render active polls list of a room
|
1
changelog.d/7879.bugfix
Normal file
1
changelog.d/7879.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Reduce number of crypto database transactions when handling the sync response
|
1
changelog.d/7899.bugfix
Normal file
1
changelog.d/7899.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[Voice Broadcast] Stop listening if we reach the last received chunk and there is no last sequence number
|
|
@ -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>
|
||||||
|
|
|
@ -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?,
|
||||||
|
)
|
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
)
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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<*, *>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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?"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
54
vector/src/main/res/layout/fragment_room_polls.xml
Normal file
54
vector/src/main/res/layout/fragment_room_polls.xml
Normal 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>
|
42
vector/src/main/res/layout/fragment_room_polls_list.xml
Normal file
42
vector/src/main/res/layout/fragment_room_polls_list.xml
Normal 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>
|
45
vector/src/main/res/layout/item_poll.xml
Normal file
45
vector/src/main/res/layout/item_poll.xml
Normal 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>
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue