Fix audit freeze, add export, and buffer gossip saves

This commit is contained in:
Valere 2020-10-28 17:40:30 +01:00
parent 5a111af2fe
commit c2027be0ee
20 changed files with 719 additions and 465 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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,7 +1112,28 @@ internal class RealmCryptoStore @Inject constructor(
return request
}
override fun saveGossipingEvents(events: List<Event>) {
val now = System.currentTimeMillis()
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(
@ -1079,11 +1146,9 @@ internal class RealmCryptoStore @Inject constructor(
decryptionResultJson = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
decryptionErrorCode = event.mCryptoError?.name
}
doRealmTransaction(realmConfiguration) { realm ->
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)

View file

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

View file

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

View file

@ -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,13 +49,9 @@ 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)
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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,19 +50,15 @@ 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)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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