diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 121d9fb401..34be1b8d05 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -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 + fun getOutgoingRoomKeyRequestsPaged(): LiveData> fun getIncomingRoomKeyRequests(): List + fun getIncomingRoomKeyRequestsPaged(): LiveData> - fun getGossipingEventsTrail(): List + fun getGossipingEventsTrail(): LiveData> + fun getGossipingEvents(): List // For testing shared session fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index f4ec7acd88..07545c50bc 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -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() + override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { 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> { + return cryptoStore.getOutgoingRoomKeyRequestsPaged() + } + + override fun getIncomingRoomKeyRequestsPaged(): LiveData> { + return cryptoStore.getIncomingRoomKeyRequestsPaged() + } + override fun getIncomingRoomKeyRequests(): List { return cryptoStore.getIncomingRoomKeyRequests() } - override fun getGossipingEventsTrail(): List { + override fun getGossipingEventsTrail(): LiveData> { return cryptoStore.getGossipingEventsTrail() } + override fun getGossipingEvents(): List { + return cryptoStore.getGossipingEvents() + } + override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap { return cryptoStore.getSharedWithInfo(roomId, sessionId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index a7c438bb31..e55cf37118 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -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" + } + )) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index a43faa2cd8..74773384ae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -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>): OutgoingSecretRequest? fun saveGossipingEvent(event: Event) + fun saveGossipingEvents(events: List) fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) { updateGossipingRequestState( @@ -442,10 +444,13 @@ internal interface IMXCryptoStore { // Dev tools fun getOutgoingRoomKeyRequests(): List + fun getOutgoingRoomKeyRequestsPaged(): LiveData> fun getOutgoingSecretKeyRequests(): List fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? fun getIncomingRoomKeyRequests(): List - fun getGossipingEventsTrail(): List + fun getIncomingRoomKeyRequestsPaged(): LiveData> + fun getGossipingEventsTrail(): LiveData> + fun getGossipingEvents(): List fun setDeviceKeysUploaded(uploaded: Boolean) fun getDeviceKeysUploaded(): Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index c0b538963d..7644ab5cc2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -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 { + override fun getIncomingRoomKeyRequestsPaged(): LiveData> { + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + realm.where() + .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> { + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + realm.where().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 { return monarchy.fetchAllCopiedSync { realm -> realm.where() }.map { @@ -1066,24 +1112,43 @@ internal class RealmCryptoStore @Inject constructor( return request } - override fun saveGossipingEvent(event: Event) { + override fun saveGossipingEvents(events: List) { 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::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::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::class.java).toJson(event.mxDecryptionResult) + decryptionErrorCode = event.mCryptoError?.name + } + realm.insertOrUpdate(entity) + } + } // override fun getOutgoingRoomKeyRequestByState(states: Set): 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> { + 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) diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsEpoxyController.kt deleted file mode 100644 index cf93bc14e7..0000000000 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsEpoxyController.kt +++ /dev/null @@ -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() { - - 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() - 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() - val content = event.getClearContent().toModel() - 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() - - 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() - 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 - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt index e2c855a9e3..0ceb8e148d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt @@ -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?) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt index d903725b22..4249ef09fa 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt @@ -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> = Uninitialized + val events: Async> = 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) {} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt new file mode 100644 index 0000000000..603c67d074 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt @@ -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( + // 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() + 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() + val content = event.getClearContent().toModel() + 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() + + 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() + 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) + } + } + } + } + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt index c7b95ddf78..35f46d9c74 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt @@ -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?) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt new file mode 100644 index 0000000000..1510d47180 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt @@ -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( + // 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}" + } + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestEpoxyController.kt deleted file mode 100644 index 5907b55b31..0000000000 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestEpoxyController.kt +++ /dev/null @@ -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() { - - 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 - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt index 22093763e1..0b0b923a48 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt @@ -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> = Uninitialized, - val outgoingRoomKeyRequests: Async> = Uninitialized + val incomingRequests: Async> = Uninitialized, + val outgoingRoomKeyRequests: Async> = 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) + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt new file mode 100644 index 0000000000..be8778e1db --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt @@ -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 = Uninitialized +) : MvRxState + +class KeyRequestViewModel @AssistedInject constructor( + @Assisted initialState: KeyRequestViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: KeyRequestViewState): KeyRequestViewModel + } + + companion object : MvRxViewModelFactory { + + @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() + 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() + val content = it.getClearContent().toModel() + + 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() + stringBuilder.append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}") + } + EventType.REQUEST_SECRET -> { + val content = it.getClearContent().toModel() + 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)) + } + } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt index 08016c66ac..0ea0e9de31 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt index 60e73fb74d..a82b5dd6c9 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt @@ -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?) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt new file mode 100644 index 0000000000..38c4c8153f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt @@ -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( + // 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}" + } + ) + } + } +} diff --git a/vector/src/main/res/layout/fragment_devtool_keyrequests.xml b/vector/src/main/res/layout/fragment_devtool_keyrequests.xml index ccd3cee660..dd0cbff1b1 100644 --- a/vector/src/main/res/layout/fragment_devtool_keyrequests.xml +++ b/vector/src/main/res/layout/fragment_devtool_keyrequests.xml @@ -1,5 +1,5 @@ - + android:layout_height="0dp" /> - \ No newline at end of file + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/menu_audit.xml b/vector/src/main/res/menu/menu_audit.xml new file mode 100644 index 0000000000..1c0d2f9989 --- /dev/null +++ b/vector/src/main/res/menu/menu_audit.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index dd461123cc..1178e5aeb4 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2304,6 +2304,7 @@ Element Android Key Requests + Export Audit Unlock encrypted messages history