mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 18:35:40 +03:00
Fix audit freeze, add export, and buffer gossip saves
This commit is contained in:
parent
5a111af2fe
commit
c2027be0ee
20 changed files with 719 additions and 465 deletions
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.crypto
|
|||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||
|
@ -40,6 +41,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
|
|||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
interface CryptoService {
|
||||
|
||||
|
@ -142,10 +144,13 @@ interface CryptoService {
|
|||
fun removeSessionListener(listener: NewSessionListener)
|
||||
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
||||
|
||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
||||
|
||||
fun getGossipingEventsTrail(): List<Event>
|
||||
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
|
||||
fun getGossipingEvents(): List<Event>
|
||||
|
||||
// For testing shared session
|
||||
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto
|
|||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import com.squareup.moshi.Types
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
@ -185,6 +186,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val gossipingBuffer = mutableListOf<Event>()
|
||||
|
||||
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
||||
setDeviceNameTask
|
||||
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
|
||||
|
@ -428,6 +431,13 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
}
|
||||
}
|
||||
|
||||
tryOrNull {
|
||||
gossipingBuffer.toList().let {
|
||||
cryptoStore.saveGossipingEvents(it)
|
||||
}
|
||||
gossipingBuffer.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -721,19 +731,19 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
when (event.getClearType()) {
|
||||
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
gossipingBuffer.add(event)
|
||||
// Keys are imported directly, not waiting for end of sync
|
||||
onRoomKeyEvent(event)
|
||||
}
|
||||
EventType.REQUEST_SECRET,
|
||||
EventType.ROOM_KEY_REQUEST -> {
|
||||
// save audit trail
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
gossipingBuffer.add(event)
|
||||
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
||||
incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
||||
}
|
||||
EventType.SEND_SECRET -> {
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
gossipingBuffer.add(event)
|
||||
onSecretSendReceived(event)
|
||||
}
|
||||
EventType.ROOM_KEY_WITHHELD -> {
|
||||
|
@ -1254,14 +1264,26 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
return cryptoStore.getOutgoingRoomKeyRequests()
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
|
||||
return cryptoStore.getOutgoingRoomKeyRequestsPaged()
|
||||
}
|
||||
|
||||
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
|
||||
return cryptoStore.getIncomingRoomKeyRequestsPaged()
|
||||
}
|
||||
|
||||
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
||||
return cryptoStore.getIncomingRoomKeyRequests()
|
||||
}
|
||||
|
||||
override fun getGossipingEventsTrail(): List<Event> {
|
||||
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
|
||||
return cryptoStore.getGossipingEventsTrail()
|
||||
}
|
||||
|
||||
override fun getGossipingEvents(): List<Event> {
|
||||
return cryptoStore.getGossipingEvents()
|
||||
}
|
||||
|
||||
override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> {
|
||||
return cryptoStore.getSharedWithInfo(roomId, sessionId)
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.MatrixCallback
|
|||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
|
@ -255,6 +256,15 @@ internal class MXMegolmEncryption(
|
|||
for ((userId, devicesToShareWith) in devicesByUser) {
|
||||
for ((deviceId) in devicesToShareWith) {
|
||||
session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex)
|
||||
cryptoStore.saveGossipingEvent(Event(
|
||||
type = EventType.ROOM_KEY,
|
||||
senderId = credentials.userId,
|
||||
content = submap.apply {
|
||||
this["session_key"] = ""
|
||||
// we add a fake key for trail
|
||||
this["_dest"] = "$userId|$deviceId"
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.internal.crypto.store
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
@ -365,6 +366,7 @@ internal interface IMXCryptoStore {
|
|||
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
|
||||
|
||||
fun saveGossipingEvent(event: Event)
|
||||
fun saveGossipingEvents(events: List<Event>)
|
||||
|
||||
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
|
||||
updateGossipingRequestState(
|
||||
|
@ -442,10 +444,13 @@ internal interface IMXCryptoStore {
|
|||
// Dev tools
|
||||
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
||||
fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest>
|
||||
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
|
||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||
fun getGossipingEventsTrail(): List<Event>
|
||||
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
||||
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
|
||||
fun getGossipingEvents(): List<Event>
|
||||
|
||||
fun setDeviceKeysUploaded(uploaded: Boolean)
|
||||
fun getDeviceKeysUploaded(): Boolean
|
||||
|
|
|
@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto.store.db
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
|
@ -62,6 +64,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFie
|
|||
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
|
@ -998,7 +1001,50 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getGossipingEventsTrail(): List<Event> {
|
||||
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||
realm.where<IncomingGossipingRequestEntity>()
|
||||
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
.sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map {
|
||||
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
|
||||
?: IncomingRoomKeyRequest(
|
||||
requestBody = null,
|
||||
deviceId = "",
|
||||
userId = "",
|
||||
requestId = "",
|
||||
state = GossipingRequestState.NONE,
|
||||
localCreationTimestamp = 0
|
||||
)
|
||||
}
|
||||
return monarchy.findAllPagedWithChanges(realmDataSourceFactory,
|
||||
LivePagedListBuilder(dataSourceFactory,
|
||||
PagedList.Config.Builder()
|
||||
.setPageSize(20)
|
||||
.setEnablePlaceholders(false)
|
||||
.setPrefetchDistance(1)
|
||||
.build())
|
||||
)
|
||||
}
|
||||
|
||||
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||
realm.where<GossipingEventEntity>().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map { it.toModel() }
|
||||
val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
|
||||
LivePagedListBuilder(dataSourceFactory,
|
||||
PagedList.Config.Builder()
|
||||
.setPageSize(20)
|
||||
.setEnablePlaceholders(false)
|
||||
.setPrefetchDistance(1)
|
||||
.build())
|
||||
)
|
||||
return trail
|
||||
}
|
||||
|
||||
override fun getGossipingEvents(): List<Event> {
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
realm.where<GossipingEventEntity>()
|
||||
}.map {
|
||||
|
@ -1066,24 +1112,43 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
return request
|
||||
}
|
||||
|
||||
override fun saveGossipingEvent(event: Event) {
|
||||
override fun saveGossipingEvents(events: List<Event>) {
|
||||
val now = System.currentTimeMillis()
|
||||
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
|
||||
val entity = GossipingEventEntity(
|
||||
type = event.type,
|
||||
sender = event.senderId,
|
||||
ageLocalTs = ageLocalTs,
|
||||
content = ContentMapper.map(event.content)
|
||||
).apply {
|
||||
sendState = SendState.SYNCED
|
||||
decryptionResultJson = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
|
||||
decryptionErrorCode = event.mCryptoError?.name
|
||||
}
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.insertOrUpdate(entity)
|
||||
monarchy.writeAsync { realm ->
|
||||
events.forEach { event ->
|
||||
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
|
||||
val entity = GossipingEventEntity(
|
||||
type = event.type,
|
||||
sender = event.senderId,
|
||||
ageLocalTs = ageLocalTs,
|
||||
content = ContentMapper.map(event.content)
|
||||
).apply {
|
||||
sendState = SendState.SYNCED
|
||||
decryptionResultJson = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
|
||||
decryptionErrorCode = event.mCryptoError?.name
|
||||
}
|
||||
realm.insertOrUpdate(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveGossipingEvent(event: Event) {
|
||||
monarchy.writeAsync { realm ->
|
||||
val now = System.currentTimeMillis()
|
||||
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
|
||||
val entity = GossipingEventEntity(
|
||||
type = event.type,
|
||||
sender = event.senderId,
|
||||
ageLocalTs = ageLocalTs,
|
||||
content = ContentMapper.map(event.content)
|
||||
).apply {
|
||||
sendState = SendState.SYNCED
|
||||
decryptionResultJson = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
|
||||
decryptionErrorCode = event.mCryptoError?.name
|
||||
}
|
||||
realm.insertOrUpdate(entity)
|
||||
}
|
||||
}
|
||||
// override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? {
|
||||
// val statesIndex = states.map { it.ordinal }.toTypedArray()
|
||||
// return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||
|
@ -1439,6 +1504,27 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
.filterNotNull()
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||
realm
|
||||
.where(OutgoingGossipingRequestEntity::class.java)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map {
|
||||
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED)
|
||||
}
|
||||
val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
|
||||
LivePagedListBuilder(dataSourceFactory,
|
||||
PagedList.Config.Builder()
|
||||
.setPageSize(20)
|
||||
.setEnablePlaceholders(false)
|
||||
.setPrefetchDistance(1)
|
||||
.build())
|
||||
)
|
||||
return trail
|
||||
}
|
||||
|
||||
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {
|
||||
return doWithRealm(realmConfiguration) { realm ->
|
||||
val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
|
||||
|
|
|
@ -1,235 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.devtools
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.core.epoxy.loadingItem
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.ui.list.GenericItem
|
||||
import im.vector.app.core.ui.list.genericFooterItem
|
||||
import im.vector.app.core.ui.list.genericItem
|
||||
import im.vector.app.core.ui.list.genericItemHeader
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
|
||||
import javax.inject.Inject
|
||||
|
||||
class GossipingEventsEpoxyController @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val vectorDateFormatter: VectorDateFormatter,
|
||||
private val colorProvider: ColorProvider
|
||||
) : TypedEpoxyController<GossipingEventsPaperTrailState>() {
|
||||
|
||||
interface InteractionListener {
|
||||
fun didTap(event: Event)
|
||||
}
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun buildModels(data: GossipingEventsPaperTrailState?) {
|
||||
when (val async = data?.events) {
|
||||
is Uninitialized,
|
||||
is Loading -> {
|
||||
loadingItem {
|
||||
id("loadingOutgoing")
|
||||
loadingText(stringProvider.getString(R.string.loading))
|
||||
}
|
||||
}
|
||||
is Fail -> {
|
||||
genericItem {
|
||||
id("failOutgoing")
|
||||
title(async.error.localizedMessage)
|
||||
}
|
||||
}
|
||||
is Success -> {
|
||||
val eventList = async.invoke()
|
||||
if (eventList.isEmpty()) {
|
||||
genericFooterItem {
|
||||
id("empty")
|
||||
text(stringProvider.getString(R.string.no_result_placeholder))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
eventList.forEachIndexed { _, event ->
|
||||
genericItem {
|
||||
id(event.hashCode())
|
||||
itemClickAction(GenericItem.Action("view").apply { perform = Runnable { interactionListener?.didTap(event) } })
|
||||
title(
|
||||
if (event.isEncrypted()) {
|
||||
"${event.getClearType()} [encrypted]"
|
||||
} else {
|
||||
event.type
|
||||
}
|
||||
)
|
||||
description(
|
||||
span {
|
||||
+vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
|
||||
span("\nfrom: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${event.senderId}"
|
||||
apply {
|
||||
if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
|
||||
val content = event.getClearContent().toModel<RoomKeyShareRequest>()
|
||||
span("\nreqId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.requestId}"
|
||||
span("\naction:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.action}"
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
span("\nsessionId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content.body?.sessionId}"
|
||||
}
|
||||
span("\nrequestedBy: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${content?.requestingDeviceId}"
|
||||
} else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
||||
val encryptedContent = event.content.toModel<OlmEventContent>()
|
||||
val content = event.getClearContent().toModel<ForwardedRoomKeyContent>()
|
||||
if (event.mxDecryptionResult == null) {
|
||||
span("**Failed to Decrypt** ${event.mCryptoError}") {
|
||||
textColor = colorProvider.getColor(R.color.vector_error_color)
|
||||
}
|
||||
}
|
||||
span("\nsessionId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.sessionId}"
|
||||
span("\nFrom Device (sender key):") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${encryptedContent?.senderKey}"
|
||||
} else if (event.getClearType() == EventType.SEND_SECRET) {
|
||||
val content = event.getClearContent().toModel<SecretSendEventContent>()
|
||||
|
||||
span("\nrequestId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.requestId}"
|
||||
span("\nFrom Device:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${event.mxDecryptionResult?.payload?.get("sender_device")}"
|
||||
} else if (event.getClearType() == EventType.REQUEST_SECRET) {
|
||||
val content = event.getClearContent().toModel<SecretShareRequest>()
|
||||
span("\nreqId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.requestId}"
|
||||
span("\naction:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.action}"
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
span("\nsecretName:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content.secretName}"
|
||||
}
|
||||
span("\nrequestedBy: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${content?.requestingDeviceId}"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildOutgoing(data: KeyRequestListViewState?) {
|
||||
data?.outgoingRoomKeyRequests?.let { async ->
|
||||
when (async) {
|
||||
is Uninitialized,
|
||||
is Loading -> {
|
||||
loadingItem {
|
||||
id("loadingOutgoing")
|
||||
loadingText(stringProvider.getString(R.string.loading))
|
||||
}
|
||||
}
|
||||
is Fail -> {
|
||||
genericItem {
|
||||
id("failOutgoing")
|
||||
title(async.error.localizedMessage)
|
||||
}
|
||||
}
|
||||
is Success -> {
|
||||
if (async.invoke().isEmpty()) {
|
||||
genericFooterItem {
|
||||
id("empty")
|
||||
text(stringProvider.getString(R.string.no_result_placeholder))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val requestList = async.invoke().groupBy { it.roomId }
|
||||
|
||||
requestList.forEach {
|
||||
genericItemHeader {
|
||||
id(it.key)
|
||||
text("roomId: ${it.key}")
|
||||
}
|
||||
it.value.forEach { roomKeyRequest ->
|
||||
genericItem {
|
||||
id(roomKeyRequest.requestId)
|
||||
title(roomKeyRequest.requestId)
|
||||
description(
|
||||
span {
|
||||
span("sessionId:\n") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${roomKeyRequest.sessionId}"
|
||||
span("\nstate:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"\n${roomKeyRequest.state.name}"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,16 +33,19 @@ import javax.inject.Inject
|
|||
|
||||
class GossipingEventsPaperTrailFragment @Inject constructor(
|
||||
val viewModelFactory: GossipingEventsPaperTrailViewModel.Factory,
|
||||
private val epoxyController: GossipingEventsEpoxyController,
|
||||
private val epoxyController: GossipingTrailPagedEpoxyController,
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment(), GossipingEventsEpoxyController.InteractionListener {
|
||||
) : VectorBaseFragment(), GossipingTrailPagedEpoxyController.InteractionListener {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_generic_recycler
|
||||
|
||||
private val viewModel: GossipingEventsPaperTrailViewModel by fragmentViewModel(GossipingEventsPaperTrailViewModel::class)
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
epoxyController.setData(state)
|
||||
state.events.invoke()?.let {
|
||||
epoxyController.submitList(it)
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
|
|
@ -16,13 +16,12 @@
|
|||
|
||||
package im.vector.app.features.settings.devtools
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagedList
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
|
@ -30,12 +29,12 @@ import com.squareup.inject.assisted.AssistedInject
|
|||
import im.vector.app.core.platform.EmptyAction
|
||||
import im.vector.app.core.platform.EmptyViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.rx.asObservable
|
||||
|
||||
data class GossipingEventsPaperTrailState(
|
||||
val events: Async<List<Event>> = Uninitialized
|
||||
val events: Async<PagedList<Event>> = Uninitialized
|
||||
) : MvRxState
|
||||
|
||||
class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState,
|
||||
|
@ -50,14 +49,10 @@ class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted i
|
|||
setState {
|
||||
copy(events = Loading())
|
||||
}
|
||||
viewModelScope.launch {
|
||||
session.cryptoService().getGossipingEventsTrail().let {
|
||||
val sorted = it.sortedByDescending { it.ageLocalTs }
|
||||
setState {
|
||||
copy(events = Success(sorted))
|
||||
session.cryptoService().getGossipingEventsTrail().asObservable()
|
||||
.execute {
|
||||
copy(events = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: EmptyAction) {}
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.devtools
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.ui.list.GenericItem
|
||||
import im.vector.app.core.ui.list.GenericItem_
|
||||
import im.vector.app.core.utils.createUIHandler
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
|
||||
import javax.inject.Inject
|
||||
|
||||
class GossipingTrailPagedEpoxyController @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val vectorDateFormatter: VectorDateFormatter,
|
||||
private val colorProvider: ColorProvider
|
||||
) : PagedListEpoxyController<Event>(
|
||||
// Important it must match the PageList builder notify Looper
|
||||
modelBuildingHandler = createUIHandler()
|
||||
) {
|
||||
|
||||
interface InteractionListener {
|
||||
fun didTap(event: Event)
|
||||
}
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> {
|
||||
val event = item ?: return GenericItem_().apply { id(currentPosition) }
|
||||
return GenericItem_().apply {
|
||||
id(event.hashCode())
|
||||
itemClickAction(GenericItem.Action("view").apply { perform = Runnable { interactionListener?.didTap(event) } })
|
||||
title(
|
||||
if (event.isEncrypted()) {
|
||||
"${event.getClearType()} [encrypted]"
|
||||
} else {
|
||||
event.type
|
||||
}
|
||||
)
|
||||
description(
|
||||
span {
|
||||
+vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
|
||||
span("\nfrom: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${event.senderId}"
|
||||
apply {
|
||||
if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
|
||||
val content = event.getClearContent().toModel<RoomKeyShareRequest>()
|
||||
span("\nreqId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.requestId}"
|
||||
span("\naction:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.action}"
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
span("\nsessionId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content.body?.sessionId}"
|
||||
}
|
||||
span("\nrequestedBy: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${content?.requestingDeviceId}"
|
||||
} else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
||||
val encryptedContent = event.content.toModel<OlmEventContent>()
|
||||
val content = event.getClearContent().toModel<ForwardedRoomKeyContent>()
|
||||
if (event.mxDecryptionResult == null) {
|
||||
span("**Failed to Decrypt** ${event.mCryptoError}") {
|
||||
textColor = colorProvider.getColor(R.color.vector_error_color)
|
||||
}
|
||||
}
|
||||
span("\nsessionId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.sessionId}"
|
||||
span("\nFrom Device (sender key):") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${encryptedContent?.senderKey}"
|
||||
} else if (event.getClearType() == EventType.ROOM_KEY) {
|
||||
// it's a bit of a fake event for trail reasons
|
||||
val content = event.getClearContent()
|
||||
span("\nsessionId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.get("session_id")}"
|
||||
span("\nroomId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.get("room_id")}"
|
||||
span("\nTo :") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.get("_dest") ?: "me"}"
|
||||
} else if (event.getClearType() == EventType.SEND_SECRET) {
|
||||
val content = event.getClearContent().toModel<SecretSendEventContent>()
|
||||
|
||||
span("\nrequestId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.requestId}"
|
||||
span("\nFrom Device:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${event.mxDecryptionResult?.payload?.get("sender_device")}"
|
||||
} else if (event.getClearType() == EventType.REQUEST_SECRET) {
|
||||
val content = event.getClearContent().toModel<SecretShareRequest>()
|
||||
span("\nreqId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.requestId}"
|
||||
span("\naction:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.action}"
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
span("\nsecretName:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content.secretName}"
|
||||
}
|
||||
span("\nrequestedBy: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${content?.requestingDeviceId}"
|
||||
} else if (event.getClearType() == EventType.ENCRYPTED) {
|
||||
span("**Failed to Decrypt** ${event.mCryptoError}") {
|
||||
textColor = colorProvider.getColor(R.color.vector_error_color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,14 +24,12 @@ 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.core.resources.ColorProvider
|
||||
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class IncomingKeyRequestListFragment @Inject constructor(
|
||||
val viewModelFactory: KeyRequestListViewModel.Factory,
|
||||
private val epoxyController: KeyRequestEpoxyController,
|
||||
private val colorProvider: ColorProvider
|
||||
private val epoxyController: IncomingKeyRequestPagedController
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_generic_recycler
|
||||
|
@ -39,8 +37,10 @@ class IncomingKeyRequestListFragment @Inject constructor(
|
|||
private val viewModel: KeyRequestListViewModel by fragmentViewModel(KeyRequestListViewModel::class)
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
epoxyController.outgoing = false
|
||||
epoxyController.setData(state)
|
||||
state.incomingRequests.invoke()?.let {
|
||||
epoxyController.submitList(it)
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.devtools
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.core.ui.list.GenericItem_
|
||||
import im.vector.app.core.utils.createUIHandler
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
|
||||
import javax.inject.Inject
|
||||
|
||||
class IncomingKeyRequestPagedController @Inject constructor(
|
||||
private val vectorDateFormatter: VectorDateFormatter
|
||||
) : PagedListEpoxyController<IncomingRoomKeyRequest>(
|
||||
// Important it must match the PageList builder notify Looper
|
||||
modelBuildingHandler = createUIHandler()
|
||||
) {
|
||||
|
||||
interface InteractionListener {
|
||||
// fun didTap(data: UserAccountData)
|
||||
}
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun buildItemModel(currentPosition: Int, item: IncomingRoomKeyRequest?): EpoxyModel<*> {
|
||||
val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) }
|
||||
|
||||
return GenericItem_().apply {
|
||||
id(roomKeyRequest.requestId)
|
||||
title(roomKeyRequest.requestId)
|
||||
description(
|
||||
span {
|
||||
span("From user: ${roomKeyRequest.userId}")
|
||||
+vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
|
||||
span("sessionId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
span("\nFrom device:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${roomKeyRequest.deviceId}"
|
||||
+"\n${roomKeyRequest.state.name}"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.devtools
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.loadingItem
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.ui.list.genericFooterItem
|
||||
import im.vector.app.core.ui.list.genericItem
|
||||
import im.vector.app.core.ui.list.genericItemHeader
|
||||
import me.gujun.android.span.span
|
||||
import javax.inject.Inject
|
||||
|
||||
class KeyRequestEpoxyController @Inject constructor(
|
||||
private val stringProvider: StringProvider
|
||||
) : TypedEpoxyController<KeyRequestListViewState>() {
|
||||
|
||||
interface InteractionListener {
|
||||
// fun didTap(data: UserAccountData)
|
||||
}
|
||||
|
||||
var outgoing = true
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun buildModels(data: KeyRequestListViewState?) {
|
||||
if (outgoing) {
|
||||
buildOutgoing(data)
|
||||
} else {
|
||||
buildIncoming(data)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildIncoming(data: KeyRequestListViewState?) {
|
||||
data?.incomingRequests?.let { async ->
|
||||
when (async) {
|
||||
is Uninitialized,
|
||||
is Loading -> {
|
||||
loadingItem {
|
||||
id("loadingOutgoing")
|
||||
loadingText(stringProvider.getString(R.string.loading))
|
||||
}
|
||||
}
|
||||
is Fail -> {
|
||||
genericItem {
|
||||
id("failOutgoing")
|
||||
title(async.error.localizedMessage)
|
||||
}
|
||||
}
|
||||
is Success -> {
|
||||
if (async.invoke().isEmpty()) {
|
||||
genericFooterItem {
|
||||
id("empty")
|
||||
text(stringProvider.getString(R.string.no_result_placeholder))
|
||||
}
|
||||
return
|
||||
}
|
||||
val requestList = async.invoke().groupBy { it.userId }
|
||||
|
||||
requestList.forEach {
|
||||
genericItemHeader {
|
||||
id(it.key)
|
||||
text("From user: ${it.key}")
|
||||
}
|
||||
it.value.forEach { roomKeyRequest ->
|
||||
genericItem {
|
||||
id(roomKeyRequest.requestId)
|
||||
title(roomKeyRequest.requestId)
|
||||
description(
|
||||
span {
|
||||
span("sessionId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
span("\nFrom device:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${roomKeyRequest.deviceId}"
|
||||
+"\n${roomKeyRequest.state.name}"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildOutgoing(data: KeyRequestListViewState?) {
|
||||
data?.outgoingRoomKeyRequests?.let { async ->
|
||||
when (async) {
|
||||
is Uninitialized,
|
||||
is Loading -> {
|
||||
loadingItem {
|
||||
id("loadingOutgoing")
|
||||
loadingText(stringProvider.getString(R.string.loading))
|
||||
}
|
||||
}
|
||||
is Fail -> {
|
||||
genericItem {
|
||||
id("failOutgoing")
|
||||
title(async.error.localizedMessage)
|
||||
}
|
||||
}
|
||||
is Success -> {
|
||||
if (async.invoke().isEmpty()) {
|
||||
genericFooterItem {
|
||||
id("empty")
|
||||
text(stringProvider.getString(R.string.no_result_placeholder))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val requestList = async.invoke().groupBy { it.roomId }
|
||||
|
||||
requestList.forEach {
|
||||
genericItemHeader {
|
||||
id(it.key)
|
||||
text("roomId: ${it.key}")
|
||||
}
|
||||
it.value.forEach { roomKeyRequest ->
|
||||
genericItem {
|
||||
id(roomKeyRequest.requestId)
|
||||
title(roomKeyRequest.requestId)
|
||||
description(
|
||||
span {
|
||||
span("sessionId:\n") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${roomKeyRequest.sessionId}"
|
||||
span("\nstate:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"\n${roomKeyRequest.state.name}"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,11 +17,11 @@
|
|||
package im.vector.app.features.settings.devtools
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagedList
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
|
@ -33,10 +33,11 @@ import kotlinx.coroutines.launch
|
|||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest
|
||||
import org.matrix.android.sdk.rx.asObservable
|
||||
|
||||
data class KeyRequestListViewState(
|
||||
val incomingRequests: Async<List<IncomingRoomKeyRequest>> = Uninitialized,
|
||||
val outgoingRoomKeyRequests: Async<List<OutgoingRoomKeyRequest>> = Uninitialized
|
||||
val incomingRequests: Async<PagedList<IncomingRoomKeyRequest>> = Uninitialized,
|
||||
val outgoingRoomKeyRequests: Async<PagedList<OutgoingRoomKeyRequest>> = Uninitialized
|
||||
) : MvRxState
|
||||
|
||||
class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState,
|
||||
|
@ -49,20 +50,16 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState
|
|||
|
||||
fun refresh() {
|
||||
viewModelScope.launch {
|
||||
session.cryptoService().getOutgoingRoomKeyRequests().let {
|
||||
setState {
|
||||
copy(
|
||||
outgoingRoomKeyRequests = Success(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
session.cryptoService().getIncomingRoomKeyRequests().let {
|
||||
setState {
|
||||
copy(
|
||||
incomingRequests = Success(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
session.cryptoService().getOutgoingRoomKeyRequestsPaged().asObservable()
|
||||
.execute {
|
||||
copy(outgoingRoomKeyRequests = it)
|
||||
}
|
||||
|
||||
session.cryptoService().getIncomingRoomKeyRequestsPaged()
|
||||
.asObservable()
|
||||
.execute {
|
||||
copy(incomingRequests = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.devtools
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
|
||||
|
||||
sealed class KeyRequestAction : VectorViewModelAction {
|
||||
data class ExportAudit(val uri: Uri) : KeyRequestAction()
|
||||
}
|
||||
|
||||
sealed class KeyRequestEvents : VectorViewEvents {
|
||||
data class SaveAudit(val uri: Uri, val raw: String) : KeyRequestEvents()
|
||||
}
|
||||
|
||||
data class KeyRequestViewState(
|
||||
val exporting: Async<String> = Uninitialized
|
||||
) : MvRxState
|
||||
|
||||
class KeyRequestViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: KeyRequestViewState,
|
||||
private val session: Session)
|
||||
: VectorViewModel<KeyRequestViewState, KeyRequestAction, KeyRequestEvents>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: KeyRequestViewState): KeyRequestViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<KeyRequestViewModel, KeyRequestViewState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: KeyRequestViewState): KeyRequestViewModel? {
|
||||
val fragment: KeyRequestsFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.viewModelFactory.create(state)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: KeyRequestAction) {
|
||||
when (action) {
|
||||
is KeyRequestAction.ExportAudit -> {
|
||||
setState {
|
||||
copy(exporting = Loading())
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// this can take long
|
||||
val eventList = session.cryptoService().getGossipingEvents()
|
||||
// clean it a bit to
|
||||
val stringBuilder = StringBuilder()
|
||||
eventList.forEach {
|
||||
val clearType = it.getClearType()
|
||||
stringBuilder.append("[${it.ageLocalTs}] : $clearType from:${it.senderId} - ")
|
||||
when (clearType) {
|
||||
EventType.ROOM_KEY_REQUEST -> {
|
||||
val content = it.getClearContent().toModel<RoomKeyShareRequest>()
|
||||
stringBuilder.append("reqId:${content?.requestId} action:${content?.action} ")
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
stringBuilder.append("sessionId: ${content.body?.sessionId} ")
|
||||
}
|
||||
stringBuilder.append("requestedBy: ${content?.requestingDeviceId} ")
|
||||
stringBuilder.append("\n")
|
||||
}
|
||||
EventType.FORWARDED_ROOM_KEY -> {
|
||||
val encryptedContent = it.content.toModel<OlmEventContent>()
|
||||
val content = it.getClearContent().toModel<ForwardedRoomKeyContent>()
|
||||
|
||||
stringBuilder.append("sessionId:${content?.sessionId} From Device (sender key):${encryptedContent?.senderKey} ")
|
||||
span("\nFrom Device (sender key):") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
stringBuilder.append("\n")
|
||||
}
|
||||
EventType.ROOM_KEY -> {
|
||||
val content = it.getClearContent()
|
||||
stringBuilder.append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}")
|
||||
stringBuilder.append("\n")
|
||||
}
|
||||
EventType.SEND_SECRET -> {
|
||||
val content = it.getClearContent().toModel<SecretSendEventContent>()
|
||||
stringBuilder.append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}")
|
||||
}
|
||||
EventType.REQUEST_SECRET -> {
|
||||
val content = it.getClearContent().toModel<SecretShareRequest>()
|
||||
stringBuilder.append("reqId:${content?.requestId} action:${content?.action} ")
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
stringBuilder.append("secretName:${content.secretName} ")
|
||||
}
|
||||
stringBuilder.append("requestedBy:${content?.requestingDeviceId}")
|
||||
stringBuilder.append("\n")
|
||||
}
|
||||
EventType.ENCRYPTED -> {
|
||||
stringBuilder.append("Failed to Derypt \n")
|
||||
}
|
||||
else -> {
|
||||
stringBuilder.append("?? \n")
|
||||
}
|
||||
}
|
||||
}
|
||||
val raw = stringBuilder.toString()
|
||||
setState {
|
||||
copy(exporting = Success(""))
|
||||
}
|
||||
_viewEvents.post(KeyRequestEvents.SaveAudit(action.uri, raw))
|
||||
} catch (error: Throwable) {
|
||||
setState {
|
||||
copy(exporting = Fail(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,20 +16,30 @@
|
|||
|
||||
package im.vector.app.features.settings.devtools
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.selectTxtFileToWrite
|
||||
import kotlinx.android.synthetic.main.fragment_devtool_keyrequests.*
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import javax.inject.Inject
|
||||
|
||||
class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() {
|
||||
class KeyRequestsFragment @Inject constructor(
|
||||
val viewModelFactory: KeyRequestViewModel.Factory) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId(): Int = R.layout.fragment_devtool_keyrequests
|
||||
|
||||
|
@ -40,6 +50,10 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() {
|
|||
|
||||
private var mPagerAdapter: KeyReqPagerAdapter? = null
|
||||
|
||||
private val viewModel: KeyRequestViewModel by fragmentViewModel()
|
||||
|
||||
override fun getMenuRes(): Int = R.menu.menu_audit
|
||||
|
||||
private val pageAdapterListener = object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
invalidateOptionsMenu()
|
||||
|
@ -53,6 +67,13 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
when (it.exporting) {
|
||||
is Loading -> exportWaitingView.isVisible = true
|
||||
else -> exportWaitingView.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
invalidateOptionsMenu()
|
||||
super.onDestroy()
|
||||
|
@ -77,6 +98,23 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() {
|
|||
}
|
||||
}
|
||||
}.attach()
|
||||
|
||||
viewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is KeyRequestEvents.SaveAudit -> {
|
||||
tryOrNull {
|
||||
val os = requireContext().contentResolver?.openOutputStream(it.uri)
|
||||
if (os == null) {
|
||||
false
|
||||
} else {
|
||||
os.write(it.raw.toByteArray())
|
||||
os.flush()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
@ -85,6 +123,28 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() {
|
|||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.audit_export) {
|
||||
selectTxtFileToWrite(
|
||||
activity = requireActivity(),
|
||||
activityResultLauncher = epxortAuditForActivityResult,
|
||||
defaultFileName = "audit-export-json_${System.currentTimeMillis()}.txt",
|
||||
chooserHint = "Export Audit"
|
||||
)
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private val epxortAuditForActivityResult = registerStartForActivityResult { activityResult ->
|
||||
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||
val uri = activityResult.data?.data
|
||||
if (uri != null) {
|
||||
viewModel.handle(KeyRequestAction.ExportAudit(uri))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class KeyReqPagerAdapter(fa: Fragment) : FragmentStateAdapter(fa) {
|
||||
override fun getItemCount(): Int = 3
|
||||
|
||||
|
|
|
@ -24,21 +24,19 @@ 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.core.resources.ColorProvider
|
||||
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class OutgoingKeyRequestListFragment @Inject constructor(
|
||||
val viewModelFactory: KeyRequestListViewModel.Factory,
|
||||
private val epoxyController: KeyRequestEpoxyController,
|
||||
private val colorProvider: ColorProvider
|
||||
private val epoxyController: OutgoingKeyRequestPagedController
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_generic_recycler
|
||||
private val viewModel: KeyRequestListViewModel by fragmentViewModel(KeyRequestListViewModel::class)
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
epoxyController.setData(state)
|
||||
epoxyController.submitList(state.outgoingRoomKeyRequests.invoke())
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.devtools
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
||||
import im.vector.app.core.ui.list.GenericItem_
|
||||
import im.vector.app.core.utils.createUIHandler
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest
|
||||
import javax.inject.Inject
|
||||
|
||||
class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController<OutgoingRoomKeyRequest>(
|
||||
// Important it must match the PageList builder notify Looper
|
||||
modelBuildingHandler = createUIHandler()
|
||||
) {
|
||||
|
||||
interface InteractionListener {
|
||||
// fun didTap(data: UserAccountData)
|
||||
}
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun buildItemModel(currentPosition: Int, item: OutgoingRoomKeyRequest?): EpoxyModel<*> {
|
||||
val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) }
|
||||
|
||||
return GenericItem_().apply {
|
||||
id(roomKeyRequest.requestId)
|
||||
title(roomKeyRequest.requestId)
|
||||
description(
|
||||
span {
|
||||
span("roomId:\n") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${roomKeyRequest.roomId}"
|
||||
|
||||
span("sessionId:\n") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${roomKeyRequest.sessionId}"
|
||||
span("\nstate:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"\n${roomKeyRequest.state.name}"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<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"
|
||||
|
@ -11,12 +11,24 @@
|
|||
android:id="@+id/devToolKeyRequestTabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tabMode="scrollable" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
app:layout_constraintTop_toBottomOf="@id/devToolKeyRequestTabs"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:id="@+id/devToolKeyRequestPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
android:layout_height="0dp" />
|
||||
|
||||
</LinearLayout>
|
||||
<ProgressBar
|
||||
android:id="@+id/exportWaitingView"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
10
vector/src/main/res/menu/menu_audit.xml
Normal file
10
vector/src/main/res/menu/menu_audit.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/audit_export"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_material_save"
|
||||
android:title="@string/settings_export_trail" />
|
||||
|
||||
</menu>
|
|
@ -2304,6 +2304,7 @@
|
|||
<string name="login_default_session_public_name">Element Android</string>
|
||||
|
||||
<string name="settings_key_requests">Key Requests</string>
|
||||
<string name="settings_export_trail">Export Audit</string>
|
||||
|
||||
<string name="e2e_use_keybackup">Unlock encrypted messages history</string>
|
||||
|
||||
|
|
Loading…
Reference in a new issue