,
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt
index 5f0515e669..82d393e79a 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt
@@ -35,6 +35,7 @@ import im.vector.matrix.android.api.session.room.model.message.FileInfo
import im.vector.matrix.android.api.session.room.model.message.ImageInfo
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent
+import im.vector.matrix.android.api.session.room.model.message.MessageContentWithFormattedBody
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageFormat
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
@@ -56,6 +57,7 @@ import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.di.UserId
+import im.vector.matrix.android.internal.extensions.subStringBetween
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
import im.vector.matrix.android.internal.task.TaskExecutor
@@ -84,6 +86,7 @@ internal class LocalEchoEventFactory @Inject constructor(
) {
// TODO Inject
private val parser = Parser.builder().build()
+
// TODO Inject
private val renderer = HtmlRenderer.builder().build()
@@ -102,8 +105,15 @@ internal class LocalEchoEventFactory @Inject constructor(
val document = parser.parse(source)
val htmlText = renderer.render(document)
- if (isFormattedTextPertinent(source, htmlText)) {
- return TextContent(text.toString(), htmlText)
+ // Cleanup extra paragraph
+ val cleanHtmlText = if (htmlText.startsWith("") && htmlText.endsWith("
\n")) {
+ htmlText.subStringBetween("", "
\n")
+ } else {
+ htmlText
+ }
+
+ if (isFormattedTextPertinent(source, cleanHtmlText)) {
+ return TextContent(text.toString(), cleanHtmlText)
}
} else {
// Try to detect pills
@@ -192,7 +202,7 @@ internal class LocalEchoEventFactory @Inject constructor(
permalink,
stringProvider.getString(R.string.message_reply_to_prefix),
userLink,
- originalEvent.getDisambiguatedDisplayName(),
+ originalEvent.senderInfo.disambiguatedDisplayName,
body.takeFormatted(),
createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted()
)
@@ -433,10 +443,8 @@ internal class LocalEchoEventFactory @Inject constructor(
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE -> {
var formattedText: String? = null
- if (content is MessageTextContent) {
- if (content.format == MessageFormat.FORMAT_MATRIX_HTML) {
- formattedText = content.formattedBody
- }
+ if (content is MessageContentWithFormattedBody) {
+ formattedText = content.matrixFormattedBody
}
val isReply = content.isReply() || originalContent.isReply()
return if (isReply) {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendResponse.kt
index 947edee1ed..6d4784d225 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendResponse.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendResponse.kt
@@ -21,5 +21,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class SendResponse(
+ /**
+ * A unique identifier for the event.
+ */
@Json(name = "event_id") val eventId: String
)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt
index f2bee734ce..95a8581c2b 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt
@@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room.timeline
import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
@@ -35,7 +36,7 @@ import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryE
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
-import im.vector.matrix.android.internal.database.query.FilterContent
+import im.vector.matrix.android.internal.database.query.TimelineEventFilter
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.database.query.whereInRoom
@@ -71,6 +72,7 @@ internal class DefaultTimeline(
private val realmConfiguration: RealmConfiguration,
private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask,
+ private val fetchNextTokenAndPaginateTask: FetchNextTokenAndPaginateTask,
private val paginationTask: PaginationTask,
private val timelineEventMapper: TimelineEventMapper,
private val settings: TimelineSettings,
@@ -383,7 +385,7 @@ internal class DefaultTimeline(
}
/**
- * This has to be called on TimelineThread as it access realm live results
+ * This has to be called on TimelineThread as it accesses realm live results
* @return true if createSnapshot should be posted
*/
private fun paginateInternal(startDisplayIndex: Int?,
@@ -446,7 +448,7 @@ internal class DefaultTimeline(
}
/**
- * This has to be called on TimelineThread as it access realm live results
+ * This has to be called on TimelineThread as it accesses realm live results
*/
private fun handleInitialLoad() {
var shouldFetchInitialEvent = false
@@ -478,7 +480,7 @@ internal class DefaultTimeline(
}
/**
- * This has to be called on TimelineThread as it access realm live results
+ * This has to be called on TimelineThread as it accesses realm live results
*/
private fun handleUpdates(results: RealmResults, changeSet: OrderedCollectionChangeSet) {
// If changeSet has deletion we are having a gap, so we clear everything
@@ -516,68 +518,90 @@ internal class DefaultTimeline(
}
/**
- * This has to be called on TimelineThread as it access realm live results
+ * This has to be called on TimelineThread as it accesses realm live results
*/
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
- val token = getTokenLive(direction)
+ val currentChunk = getLiveChunk()
+ val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken
if (token == null) {
- updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
- return
- }
- val params = PaginationTask.Params(roomId = roomId,
- from = token,
- direction = direction.toPaginationDirection(),
- limit = limit)
-
- Timber.v("Should fetch $limit items $direction")
- cancelableBag += paginationTask
- .configureWith(params) {
- this.callback = object : MatrixCallback {
- override fun onSuccess(data: TokenChunkEventPersistor.Result) {
- when (data) {
- TokenChunkEventPersistor.Result.SUCCESS -> {
- Timber.v("Success fetching $limit items $direction from pagination request")
- }
- TokenChunkEventPersistor.Result.REACHED_END -> {
- postSnapshot()
- }
- TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
- // Database won't be updated, so we force pagination request
- BACKGROUND_HANDLER.post {
- executePaginationTask(direction, limit)
- }
+ if (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse()) {
+ // We are in the case that next event exists, but we do not know the next token.
+ // Fetch (again) the last event to get a nextToken
+ val lastKnownEventId = nonFilteredEvents.firstOrNull()?.eventId
+ if (lastKnownEventId == null) {
+ updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
+ } else {
+ val params = FetchNextTokenAndPaginateTask.Params(
+ roomId = roomId,
+ limit = limit,
+ lastKnownEventId = lastKnownEventId
+ )
+ cancelableBag += fetchNextTokenAndPaginateTask
+ .configureWith(params) {
+ this.callback = createPaginationCallback(limit, direction)
}
- }
+ .executeBy(taskExecutor)
+ }
+ } else {
+ updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
+ }
+ } else {
+ val params = PaginationTask.Params(
+ roomId = roomId,
+ from = token,
+ direction = direction.toPaginationDirection(),
+ limit = limit
+ )
+ Timber.v("Should fetch $limit items $direction")
+ cancelableBag += paginationTask
+ .configureWith(params) {
+ this.callback = createPaginationCallback(limit, direction)
+ }
+ .executeBy(taskExecutor)
+ }
+ }
- override fun onFailure(failure: Throwable) {
- updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
- postSnapshot()
- Timber.v("Failure fetching $limit items $direction from pagination request")
+ // For debug purpose only
+ private fun dumpAndLogChunks() {
+ val liveChunk = getLiveChunk()
+ Timber.w("Live chunk: $liveChunk")
+
+ Realm.getInstance(realmConfiguration).use { realm ->
+ ChunkEntity.where(realm, roomId).findAll()
+ .also { Timber.w("Found ${it.size} chunks") }
+ .forEach {
+ Timber.w("")
+ Timber.w("ChunkEntity: $it")
+ Timber.w("prevToken: ${it.prevToken}")
+ Timber.w("nextToken: ${it.nextToken}")
+ Timber.w("isLastBackward: ${it.isLastBackward}")
+ Timber.w("isLastForward: ${it.isLastForward}")
+ it.timelineEvents.forEach { tle ->
+ Timber.w(" TLE: ${tle.root?.content}")
}
}
- }
- .executeBy(taskExecutor)
+ }
}
/**
- * This has to be called on TimelineThread as it access realm live results
+ * This has to be called on TimelineThread as it accesses realm live results
*/
-
private fun getTokenLive(direction: Timeline.Direction): String? {
val chunkEntity = getLiveChunk() ?: return null
return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
}
/**
- * This has to be called on TimelineThread as it access realm live results
+ * This has to be called on TimelineThread as it accesses realm live results
+ * Return the current Chunk
*/
private fun getLiveChunk(): ChunkEntity? {
return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull()
}
/**
- * This has to be called on TimelineThread as it access realm live results
- * @return number of items who have been added
+ * This has to be called on TimelineThread as it accesses realm live results
+ * @return the number of items who have been added
*/
private fun buildTimelineEvents(startDisplayIndex: Int?,
direction: Timeline.Direction,
@@ -618,6 +642,8 @@ internal class DefaultTimeline(
}
val time = System.currentTimeMillis() - start
Timber.v("Built ${offsetResults.size} items from db in $time ms")
+ // For the case where wo reach the lastForward chunk
+ updateLoadingStates(filteredEvents)
return offsetResults.size
}
@@ -628,7 +654,7 @@ internal class DefaultTimeline(
)
/**
- * This has to be called on TimelineThread as it access realm live results
+ * This has to be called on TimelineThread as it accesses realm live results
*/
private fun getOffsetResults(startDisplayIndex: Int,
direction: Timeline.Direction,
@@ -713,6 +739,32 @@ internal class DefaultTimeline(
forwardsState.set(State())
}
+ private fun createPaginationCallback(limit: Int, direction: Timeline.Direction): MatrixCallback {
+ return object : MatrixCallback {
+ override fun onSuccess(data: TokenChunkEventPersistor.Result) {
+ when (data) {
+ TokenChunkEventPersistor.Result.SUCCESS -> {
+ Timber.v("Success fetching $limit items $direction from pagination request")
+ }
+ TokenChunkEventPersistor.Result.REACHED_END -> {
+ postSnapshot()
+ }
+ TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
+ // Database won't be updated, so we force pagination request
+ BACKGROUND_HANDLER.post {
+ executePaginationTask(direction, limit)
+ }
+ }
+ }
+
+ override fun onFailure(failure: Throwable) {
+ updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
+ postSnapshot()
+ Timber.v("Failure fetching $limit items $direction from pagination request")
+ }
+ }
+ }
+
// Extension methods ***************************************************************************
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
@@ -724,8 +776,11 @@ internal class DefaultTimeline(
`in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray())
}
if (settings.filterEdits) {
- not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE)
- not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE)
+ not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
+ not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
+ }
+ if (settings.filterRedacted) {
+ not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
}
return this
}
@@ -737,13 +792,19 @@ internal class DefaultTimeline(
} else {
true
}
+ if (!filterType) return@filter false
+
val filterEdits = if (settings.filterEdits && it.root.type == EventType.MESSAGE) {
val messageContent = it.root.content.toModel()
messageContent?.relatesTo?.type != RelationType.REPLACE
} else {
true
}
- filterType && filterEdits
+ if (!filterEdits) return@filter false
+
+ val filterRedacted = settings.filterRedacted && it.root.isRedacted()
+
+ filterRedacted
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt
index c02bb915ef..ffa282d088 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt
@@ -42,6 +42,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
private val contextOfEventTask: GetContextOfEventTask,
private val eventDecryptor: TimelineEventDecryptor,
private val paginationTask: PaginationTask,
+ private val fetchNextTokenAndPaginateTask: FetchNextTokenAndPaginateTask,
private val timelineEventMapper: TimelineEventMapper,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper
) : TimelineService {
@@ -63,7 +64,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
settings = settings,
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
eventBus = eventBus,
- eventDecryptor = eventDecryptor
+ eventDecryptor = eventDecryptor,
+ fetchNextTokenAndPaginateTask = fetchNextTokenAndPaginateTask
)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchNextTokenAndPaginateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchNextTokenAndPaginateTask.kt
new file mode 100644
index 0000000000..1189e627c4
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchNextTokenAndPaginateTask.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.matrix.android.internal.session.room.timeline
+
+import com.zhuinden.monarchy.Monarchy
+import im.vector.matrix.android.internal.database.model.ChunkEntity
+import im.vector.matrix.android.internal.database.query.findIncludingEvent
+import im.vector.matrix.android.internal.network.executeRequest
+import im.vector.matrix.android.internal.session.filter.FilterRepository
+import im.vector.matrix.android.internal.session.room.RoomAPI
+import im.vector.matrix.android.internal.task.Task
+import im.vector.matrix.android.internal.util.awaitTransaction
+import org.greenrobot.eventbus.EventBus
+import javax.inject.Inject
+
+internal interface FetchNextTokenAndPaginateTask : Task {
+
+ data class Params(
+ val roomId: String,
+ val lastKnownEventId: String,
+ val limit: Int
+ )
+}
+
+internal class DefaultFetchNextTokenAndPaginateTask @Inject constructor(
+ private val roomAPI: RoomAPI,
+ private val monarchy: Monarchy,
+ private val filterRepository: FilterRepository,
+ private val paginationTask: PaginationTask,
+ private val eventBus: EventBus
+) : FetchNextTokenAndPaginateTask {
+
+ override suspend fun execute(params: FetchNextTokenAndPaginateTask.Params): TokenChunkEventPersistor.Result {
+ val filter = filterRepository.getRoomFilter()
+ val response = executeRequest(eventBus) {
+ apiCall = roomAPI.getContextOfEvent(params.roomId, params.lastKnownEventId, 0, filter)
+ }
+ if (response.end == null) {
+ throw IllegalStateException("No next token found")
+ }
+ monarchy.awaitTransaction {
+ ChunkEntity.findIncludingEvent(it, params.lastKnownEventId)?.nextToken = response.end
+ }
+ val paginationParams = PaginationTask.Params(
+ roomId = params.roomId,
+ from = response.end,
+ direction = PaginationDirection.FORWARDS,
+ limit = params.limit
+ )
+ return paginationTask.execute(paginationParams)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt
index 056f942211..72e99701cd 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt
@@ -24,7 +24,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntit
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
-import im.vector.matrix.android.internal.database.query.FilterContent
+import im.vector.matrix.android.internal.database.query.TimelineEventFilter
import im.vector.matrix.android.internal.database.query.whereInRoom
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
@@ -149,16 +149,21 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu
*/
private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery {
beginGroup()
+ var needOr = false
if (settings.filterTypes) {
not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray())
- }
- if (settings.filterTypes && settings.filterEdits) {
- or()
+ needOr = true
}
if (settings.filterEdits) {
- like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE)
+ if (needOr) or()
+ like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.EDIT)
or()
- like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.RESPONSE_TYPE)
+ like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.RESPONSE)
+ needOr = true
+ }
+ if (settings.filterRedacted) {
+ if (needOr) or()
+ like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED)
}
endGroup()
return this
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt
index 95edf9bc49..7344f5598b 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt
@@ -23,4 +23,6 @@ internal interface TokenChunkEvent {
val end: String?
val events: List
val stateEvents: List
+
+ fun hasMore() = start != end
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt
index 164626224b..e0f5b106d6 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt
@@ -35,7 +35,7 @@ import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore
import im.vector.matrix.android.internal.database.query.create
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
-import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
+import im.vector.matrix.android.internal.database.query.findLastForwardChunkOfRoom
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.where
@@ -149,7 +149,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
}
?: ChunkEntity.create(realm, prevToken, nextToken)
- if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
+ if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) {
handleReachEnd(realm, roomId, direction, currentChunk)
} else {
handlePagination(realm, roomId, direction, receivedChunk, currentChunk)
@@ -169,10 +169,10 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
Timber.v("Reach end of $roomId")
if (direction == PaginationDirection.FORWARDS) {
- val currentLiveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
- if (currentChunk != currentLiveChunk) {
+ val currentLastForwardChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
+ if (currentChunk != currentLastForwardChunk) {
currentChunk.isLastForward = true
- currentLiveChunk?.deleteOnCascade()
+ currentLastForwardChunk?.deleteOnCascade()
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
latestPreviewableEvent = TimelineEventEntity.latestEvent(
realm,
@@ -224,10 +224,13 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
}
+ // Find all the chunks which contain at least one event from the list of eventIds
val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds)
+ Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds")
val chunksToDelete = ArrayList()
chunks.forEach {
if (it != currentChunk) {
+ Timber.d("Merge $it")
currentChunk.merge(roomId, it, direction)
chunksToDelete.add(it)
}
@@ -246,6 +249,8 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
)
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
}
- RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk)
+ if (currentChunk.isValid) {
+ RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk)
+ }
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt
new file mode 100644
index 0000000000..6ec9abf8e2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.matrix.android.internal.session.room.uploads
+
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.crypto.CryptoService
+import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult
+import im.vector.matrix.android.api.session.room.uploads.UploadsService
+import im.vector.matrix.android.api.util.Cancelable
+import im.vector.matrix.android.internal.task.TaskExecutor
+import im.vector.matrix.android.internal.task.configureWith
+
+internal class DefaultUploadsService @AssistedInject constructor(
+ @Assisted private val roomId: String,
+ private val taskExecutor: TaskExecutor,
+ private val getUploadsTask: GetUploadsTask,
+ private val cryptoService: CryptoService
+) : UploadsService {
+
+ @AssistedInject.Factory
+ interface Factory {
+ fun create(roomId: String): UploadsService
+ }
+
+ override fun getUploads(numberOfEvents: Int, since: String?, callback: MatrixCallback): Cancelable {
+ return getUploadsTask
+ .configureWith(GetUploadsTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), numberOfEvents, since)) {
+ this.callback = callback
+ }
+ .executeBy(taskExecutor)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt
new file mode 100644
index 0000000000..ecc7bc1b41
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.matrix.android.internal.session.room.uploads
+
+import com.zhuinden.monarchy.Monarchy
+import im.vector.matrix.android.api.session.events.model.Event
+import im.vector.matrix.android.api.session.events.model.EventType
+import im.vector.matrix.android.api.session.events.model.toModel
+import im.vector.matrix.android.api.session.room.model.message.MessageContent
+import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
+import im.vector.matrix.android.api.session.room.sender.SenderInfo
+import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult
+import im.vector.matrix.android.api.session.room.uploads.UploadEvent
+import im.vector.matrix.android.internal.database.mapper.asDomain
+import im.vector.matrix.android.internal.database.model.EventEntity
+import im.vector.matrix.android.internal.database.model.EventEntityFields
+import im.vector.matrix.android.internal.database.query.TimelineEventFilter
+import im.vector.matrix.android.internal.database.query.whereType
+import im.vector.matrix.android.internal.network.executeRequest
+import im.vector.matrix.android.internal.session.filter.FilterFactory
+import im.vector.matrix.android.internal.session.room.RoomAPI
+import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
+import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
+import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
+import im.vector.matrix.android.internal.session.sync.SyncTokenStore
+import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
+import javax.inject.Inject
+
+internal interface GetUploadsTask : Task {
+
+ data class Params(
+ val roomId: String,
+ val isRoomEncrypted: Boolean,
+ val numberOfEvents: Int,
+ val since: String?
+ )
+}
+
+internal class DefaultGetUploadsTask @Inject constructor(
+ private val roomAPI: RoomAPI,
+ private val tokenStore: SyncTokenStore,
+ private val monarchy: Monarchy,
+ private val eventBus: EventBus)
+ : GetUploadsTask {
+
+ override suspend fun execute(params: GetUploadsTask.Params): GetUploadsResult {
+ val result: GetUploadsResult
+ val events: List
+
+ if (params.isRoomEncrypted) {
+ // Get a chunk of events from cache for e2e rooms
+
+ result = GetUploadsResult(
+ uploadEvents = emptyList(),
+ nextToken = "",
+ hasMore = false
+ )
+
+ var eventsFromRealm = emptyList()
+ monarchy.doWithRealm { realm ->
+ eventsFromRealm = EventEntity.whereType(realm, EventType.ENCRYPTED, params.roomId)
+ .like(EventEntityFields.DECRYPTION_RESULT_JSON, TimelineEventFilter.DecryptedContent.URL)
+ .findAll()
+ .map { it.asDomain() }
+ // Exclude stickers
+ .filter { it.getClearType() != EventType.STICKER }
+ }
+ events = eventsFromRealm
+ } else {
+ val since = params.since ?: tokenStore.getLastToken() ?: throw IllegalStateException("No token available")
+
+ val filter = FilterFactory.createUploadsFilter(params.numberOfEvents).toJSONString()
+ val chunk = executeRequest(eventBus) {
+ apiCall = roomAPI.getRoomMessagesFrom(params.roomId, since, PaginationDirection.BACKWARDS.value, params.numberOfEvents, filter)
+ }
+
+ result = GetUploadsResult(
+ uploadEvents = emptyList(),
+ nextToken = chunk.end ?: "",
+ hasMore = chunk.hasMore()
+ )
+ events = chunk.events
+ }
+
+ var uploadEvents = listOf()
+
+ val cacheOfSenderInfos = mutableMapOf()
+
+ // Get a snapshot of all room members
+ monarchy.doWithRealm { realm ->
+ val roomMemberHelper = RoomMemberHelper(realm, params.roomId)
+
+ uploadEvents = events.mapNotNull { event ->
+ val eventId = event.eventId ?: return@mapNotNull null
+ val messageContent = event.getClearContent()?.toModel() ?: return@mapNotNull null
+ val messageWithAttachmentContent = (messageContent as? MessageWithAttachmentContent) ?: return@mapNotNull null
+ val senderId = event.senderId ?: return@mapNotNull null
+
+ val senderInfo = cacheOfSenderInfos.getOrPut(senderId) {
+ val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(senderId)
+ SenderInfo(
+ userId = senderId,
+ displayName = roomMemberSummaryEntity?.displayName,
+ isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName),
+ avatarUrl = roomMemberSummaryEntity?.avatarUrl
+ )
+ }
+
+ UploadEvent(
+ root = event,
+ eventId = eventId,
+ contentWithAttachmentContent = messageWithAttachmentContent,
+ senderInfo = senderInfo
+ )
+ }
+ }
+
+ return result.copy(uploadEvents = uploadEvents)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt
index 68df456831..021b3ed066 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt
@@ -25,7 +25,6 @@ import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
-import kotlinx.coroutines.GlobalScope
import javax.inject.Inject
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
@@ -45,7 +44,7 @@ internal class DefaultSignOutService @Inject constructor(private val signOutTask
override fun updateCredentials(credentials: Credentials,
callback: MatrixCallback): Cancelable {
- return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
sessionParamsStore.updateCredentials(credentials)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt
index 0b8902e71b..5763d397c8 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt
@@ -43,13 +43,13 @@ internal class DefaultSignInAgainTask @Inject constructor(
apiCall = signOutAPI.loginAgain(
PasswordLoginParams.userIdentifier(
// Reuse the same userId
- sessionParams.credentials.userId,
+ sessionParams.userId,
params.password,
// The spec says the initial device name will be ignored
// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login
// but https://github.com/matrix-org/synapse/issues/6525
// Reuse the same deviceId
- deviceId = sessionParams.credentials.deviceId
+ deviceId = sessionParams.deviceId
)
)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
index 610ade5744..cca0af7feb 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
@@ -20,6 +20,7 @@ import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.cleanup.CleanupSession
+import im.vector.matrix.android.internal.session.identity.IdentityDisconnectTask
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
@@ -35,6 +36,7 @@ internal interface SignOutTask : Task {
internal class DefaultSignOutTask @Inject constructor(
private val signOutAPI: SignOutAPI,
private val eventBus: EventBus,
+ private val identityDisconnectTask: IdentityDisconnectTask,
private val cleanupSession: CleanupSession
) : SignOutTask {
@@ -60,6 +62,10 @@ internal class DefaultSignOutTask @Inject constructor(
}
}
+ // Logout from identity server if any
+ runCatching { identityDisconnectTask.execute(Unit) }
+ .onFailure { Timber.w(it, "Unable to disconnect identity server") }
+
Timber.d("SignOut: cleanup session...")
cleanupSession.handle()
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
index 70c1e39334..a910944fbf 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
@@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addTimelineEvent
+import im.vector.matrix.android.internal.database.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
@@ -36,7 +37,7 @@ import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore
import im.vector.matrix.android.internal.database.query.find
-import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
+import im.vector.matrix.android.internal.database.query.findLastForwardChunkOfRoom
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.getOrNull
import im.vector.matrix.android.internal.database.query.where
@@ -220,12 +221,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
prevToken: String? = null,
isLimited: Boolean = true,
syncLocalTimestampMillis: Long): ChunkEntity {
- val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
+ val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
val chunkEntity = if (!isLimited && lastChunk != null) {
lastChunk
} else {
realm.createObject().apply { this.prevToken = prevToken }
}
+ // Only one chunk has isLastForward set to true
lastChunk?.isLastForward = false
chunkEntity.isLastForward = true
@@ -271,6 +273,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
event.mxDecryptionResult = adapter.fromJson(json)
}
}
+ // Finally delete the local echo
+ sendingEventEntity.deleteOnCascade()
} else {
Timber.v("Can't find corresponding local echo for tx:$it")
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt
index c508413665..c2e36604e3 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt
@@ -30,5 +30,7 @@ abstract class UserAccountData : AccountDataContent {
const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls"
const val TYPE_WIDGETS = "m.widgets"
const val TYPE_PUSH_RULES = "m.push_rules"
+ const val TYPE_IDENTITY_SERVER = "m.identity_server"
+ const val TYPE_ACCEPTED_TERMS = "m.accepted_terms"
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAcceptedTerms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAcceptedTerms.kt
new file mode 100644
index 0000000000..ef34503463
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAcceptedTerms.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.matrix.android.internal.session.sync.model.accountdata
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class UserAccountDataAcceptedTerms(
+ @Json(name = "type") override val type: String = TYPE_ACCEPTED_TERMS,
+ @Json(name = "content") val content: AcceptedTermsContent
+) : UserAccountData()
+
+@JsonClass(generateAdapter = true)
+internal data class AcceptedTermsContent(
+ @Json(name = "accepted") val acceptedTerms: List = emptyList()
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentityServer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentityServer.kt
new file mode 100644
index 0000000000..4af2034d64
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentityServer.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.matrix.android.internal.session.sync.model.accountdata
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class UserAccountDataIdentityServer(
+ @Json(name = "type") override val type: String = TYPE_IDENTITY_SERVER,
+ @Json(name = "content") val content: IdentityServerContent? = null
+) : UserAccountData()
+
+@JsonClass(generateAdapter = true)
+internal data class IdentityServerContent(
+ @Json(name = "base_url") val baseUrl: String? = null
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt
new file mode 100644
index 0000000000..c5827b822f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.matrix.android.internal.session.terms
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * This class represent a list of urls of terms the user wants to accept
+ */
+@JsonClass(generateAdapter = true)
+internal data class AcceptTermsBody(
+ @Json(name = "user_accepts")
+ val acceptedTermUrls: List
+)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt
new file mode 100644
index 0000000000..6d5e597da8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.matrix.android.internal.session.terms
+
+import dagger.Lazy
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.events.model.toModel
+import im.vector.matrix.android.api.session.terms.GetTermsResponse
+import im.vector.matrix.android.api.session.terms.TermsService
+import im.vector.matrix.android.api.util.Cancelable
+import im.vector.matrix.android.internal.di.Unauthenticated
+import im.vector.matrix.android.internal.network.NetworkConstants
+import im.vector.matrix.android.internal.network.RetrofitFactory
+import im.vector.matrix.android.internal.network.executeRequest
+import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI
+import im.vector.matrix.android.internal.session.identity.IdentityRegisterTask
+import im.vector.matrix.android.internal.session.identity.todelete.AccountDataDataSource
+import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask
+import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent
+import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
+import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
+import im.vector.matrix.android.internal.task.TaskExecutor
+import im.vector.matrix.android.internal.task.launchToCallback
+import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
+import okhttp3.OkHttpClient
+import javax.inject.Inject
+
+internal class DefaultTermsService @Inject constructor(
+ @Unauthenticated
+ private val unauthenticatedOkHttpClient: Lazy,
+ private val accountDataDataSource: AccountDataDataSource,
+ private val termsAPI: TermsAPI,
+ private val retrofitFactory: RetrofitFactory,
+ private val getOpenIdTokenTask: GetOpenIdTokenTask,
+ private val identityRegisterTask: IdentityRegisterTask,
+ private val updateUserAccountDataTask: UpdateUserAccountDataTask,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
+ private val taskExecutor: TaskExecutor
+) : TermsService {
+ override fun getTerms(serviceType: TermsService.ServiceType,
+ baseUrl: String,
+ callback: MatrixCallback): Cancelable {
+ return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
+ val sep = if (baseUrl.endsWith("/")) "" else "/"
+
+ val url = when (serviceType) {
+ TermsService.ServiceType.IntegrationManager -> "$baseUrl$sep${NetworkConstants.URI_INTEGRATION_MANAGER_PATH}"
+ TermsService.ServiceType.IdentityService -> "$baseUrl$sep${NetworkConstants.URI_IDENTITY_PATH_V2}"
+ }
+
+ val termsResponse = executeRequest(null) {
+ apiCall = termsAPI.getTerms("${url}terms")
+ }
+
+ GetTermsResponse(termsResponse, getAlreadyAcceptedTermUrlsFromAccountData())
+ }
+ }
+
+ override fun agreeToTerms(serviceType: TermsService.ServiceType,
+ baseUrl: String,
+ agreedUrls: List,
+ token: String?,
+ callback: MatrixCallback): Cancelable {
+ return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
+ val sep = if (baseUrl.endsWith("/")) "" else "/"
+
+ val url = when (serviceType) {
+ TermsService.ServiceType.IntegrationManager -> "$baseUrl$sep${NetworkConstants.URI_INTEGRATION_MANAGER_PATH}"
+ TermsService.ServiceType.IdentityService -> "$baseUrl$sep${NetworkConstants.URI_IDENTITY_PATH_V2}"
+ }
+
+ val tokenToUse = token?.takeIf { it.isNotEmpty() } ?: getToken(baseUrl)
+
+ executeRequest(null) {
+ apiCall = termsAPI.agreeToTerms("${url}terms", AcceptTermsBody(agreedUrls), "Bearer $tokenToUse")
+ }
+
+ // client SHOULD update this account data section adding any the URLs
+ // of any additional documents that the user agreed to this list.
+ // Get current m.accepted_terms append new ones and update account data
+ val listOfAcceptedTerms = getAlreadyAcceptedTermUrlsFromAccountData()
+
+ val newList = listOfAcceptedTerms.toMutableSet().apply { addAll(agreedUrls) }.toList()
+
+ updateUserAccountDataTask.execute(UpdateUserAccountDataTask.AcceptedTermsParams(
+ acceptedTermsContent = AcceptedTermsContent(newList)
+ ))
+ }
+ }
+
+ private suspend fun getToken(url: String): String {
+ // TODO This is duplicated code see DefaultIdentityService
+ val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
+
+ val openIdToken = getOpenIdTokenTask.execute(Unit)
+ val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken))
+
+ return token.token
+ }
+
+ private fun getAlreadyAcceptedTermUrlsFromAccountData(): Set {
+ return accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ACCEPTED_TERMS)
+ ?.content
+ ?.toModel()
+ ?.acceptedTerms
+ ?.toSet()
+ .orEmpty()
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt
similarity index 53%
rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt
rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt
index 2a0e00704c..03b745f8d7 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt
@@ -13,22 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package im.vector.matrix.android.internal.identity
-import im.vector.matrix.android.internal.network.NetworkConstants
+package im.vector.matrix.android.internal.session.terms
+
+import im.vector.matrix.android.internal.network.HttpHeaders
import retrofit2.Call
+import retrofit2.http.Body
import retrofit2.http.GET
+import retrofit2.http.Header
+import retrofit2.http.POST
+import retrofit2.http.Url
-internal interface IdentityPingApi {
+internal interface TermsAPI {
+ /**
+ * This request does not require authentication
+ */
+ @GET
+ fun getTerms(@Url url: String): Call
/**
- * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
- * Simple ping call to check if server alive
- *
- * Ref: https://matrix.org/docs/spec/identity_service/unstable#status-check
- *
- * @return 200 in case of success
+ * This request requires authentication
*/
- @GET(NetworkConstants.URI_API_PREFIX_IDENTITY)
- fun ping(): Call