forked from GitHub-Mirror/riotX-android
Merge branch 'feature/media_attachment' into develop
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'realm-android'
|
||||
apply plugin: 'okreplay'
|
||||
|
||||
@ -19,6 +20,10 @@ repositories {
|
||||
jcenter()
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.group.GroupService
|
||||
@ -58,6 +59,11 @@ interface Session :
|
||||
*/
|
||||
fun contentUrlResolver(): ContentUrlResolver
|
||||
|
||||
/**
|
||||
* Returns the ContentUploadProgressTracker associated with the session
|
||||
*/
|
||||
fun contentUploadProgressTracker(): ContentUploadStateTracker
|
||||
|
||||
/**
|
||||
* Add a listener to the session.
|
||||
* @param listener the listener to add.
|
||||
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2019 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.api.session.content
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class ContentAttachmentData(
|
||||
val size: Long = 0,
|
||||
val duration: Long? = 0,
|
||||
val date: Long = 0,
|
||||
val height: Long? = 0,
|
||||
val width: Long? = 0,
|
||||
val name: String? = null,
|
||||
val path: String? = null,
|
||||
val mimeType: String? = null,
|
||||
val type: Type
|
||||
) : Parcelable {
|
||||
|
||||
enum class Type {
|
||||
FILE,
|
||||
IMAGE,
|
||||
AUDIO,
|
||||
VIDEO
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2019 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.api.session.content
|
||||
|
||||
interface ContentUploadStateTracker {
|
||||
|
||||
fun track(eventId: String, updateListener: UpdateListener)
|
||||
|
||||
fun untrack(eventId: String, updateListener: UpdateListener)
|
||||
|
||||
interface UpdateListener {
|
||||
fun onUpdate(state: State)
|
||||
}
|
||||
|
||||
sealed class State {
|
||||
object Idle : State()
|
||||
data class ProgressData(val current: Long, val total: Long) : State()
|
||||
object Success : State()
|
||||
object Failure : State()
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -22,8 +22,8 @@ import com.squareup.moshi.JsonClass
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VideoInfo(
|
||||
@Json(name = "mimetype") val mimeType: String,
|
||||
@Json(name = "w") val w: Int = 0,
|
||||
@Json(name = "h") val h: Int = 0,
|
||||
@Json(name = "w") val width: Int = 0,
|
||||
@Json(name = "h") val height: Int = 0,
|
||||
@Json(name = "size") val size: Long = 0,
|
||||
@Json(name = "duration") val duration: Int = 0,
|
||||
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,
|
||||
|
@ -16,11 +16,11 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.send
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
|
||||
/**
|
||||
* This interface defines methods to send events in a room. It's implemented at the room level.
|
||||
*/
|
||||
@ -30,12 +30,23 @@ interface SendService {
|
||||
* Method to send a text message asynchronously.
|
||||
* @param text the text message to send
|
||||
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
|
||||
* @param callback the callback to be notified.
|
||||
* @return a [Cancelable]
|
||||
*/
|
||||
fun sendTextMessage(text: String,
|
||||
msgType: String = MessageType.MSGTYPE_TEXT,
|
||||
callback: MatrixCallback<Event>): Cancelable
|
||||
fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable
|
||||
|
||||
/**
|
||||
* Method to send a media asynchronously.
|
||||
* @param attachment the media to send
|
||||
* @return a [Cancelable]
|
||||
*/
|
||||
fun sendMedia(attachment: ContentAttachmentData): Cancelable
|
||||
|
||||
/**
|
||||
* Method to send a list of media asynchronously.
|
||||
* @param attachments the list of media to send
|
||||
* @return a [Cancelable]
|
||||
*/
|
||||
fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2019 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.api.session.room.send
|
||||
|
||||
enum class SendState {
|
||||
UNKNOWN,
|
||||
UNSENT,
|
||||
ENCRYPTING,
|
||||
SENDING,
|
||||
SENT,
|
||||
SYNCED;
|
||||
|
||||
fun isSent(): Boolean {
|
||||
return this == SENT || this == SYNCED
|
||||
}
|
||||
|
||||
}
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.timeline
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
|
||||
/**
|
||||
* This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline.
|
||||
@ -28,7 +29,8 @@ data class TimelineEvent(
|
||||
val root: Event,
|
||||
val localId: String,
|
||||
val displayIndex: Int,
|
||||
val roomMember: RoomMember?
|
||||
val roomMember: RoomMember?,
|
||||
val sendState: SendState
|
||||
) {
|
||||
|
||||
val metadata = HashMap<String, Any>()
|
||||
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2019 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.database
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmChangeListener
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
private const val THREAD_NAME = "REALM_QUERY_LATCH"
|
||||
|
||||
class RealmQueryLatch<E : RealmObject>(private val realmConfiguration: RealmConfiguration,
|
||||
private val realmQueryBuilder: (Realm) -> RealmQuery<E>) {
|
||||
|
||||
fun await() {
|
||||
val latch = CountDownLatch(1)
|
||||
val handlerThread = HandlerThread(THREAD_NAME + hashCode())
|
||||
handlerThread.start()
|
||||
val handler = Handler(handlerThread.looper)
|
||||
val runnable = Runnable {
|
||||
val realm = Realm.getInstance(realmConfiguration)
|
||||
val result = realmQueryBuilder(realm).findAllAsync()
|
||||
result.addChangeListener(object : RealmChangeListener<RealmResults<E>> {
|
||||
override fun onChange(t: RealmResults<E>) {
|
||||
if (t.isNotEmpty()) {
|
||||
result.removeChangeListener(this)
|
||||
realm.close()
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
handler.post(runnable)
|
||||
latch.await()
|
||||
handlerThread.quit()
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.database.helper
|
||||
|
||||
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.room.send.SendState
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.mapper.toEntity
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
@ -89,9 +90,10 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
isUnlinked: Boolean = false) {
|
||||
|
||||
assertIsManaged()
|
||||
if (event.eventId.isNullOrEmpty() || events.fastContains(event.eventId)) {
|
||||
if (event.eventId.isNullOrEmpty() || this.events.fastContains(event.eventId)) {
|
||||
return
|
||||
}
|
||||
|
||||
var currentDisplayIndex = lastDisplayIndex(direction, 0)
|
||||
if (direction == PaginationDirection.FORWARDS) {
|
||||
currentDisplayIndex += 1
|
||||
@ -115,6 +117,7 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
this.stateIndex = currentStateIndex
|
||||
this.isUnlinked = isUnlinked
|
||||
this.displayIndex = currentDisplayIndex
|
||||
this.sendState = SendState.SYNCED
|
||||
}
|
||||
// We are not using the order of the list, but will be sorting with displayIndex field
|
||||
events.add(eventEntity)
|
||||
@ -122,14 +125,14 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
|
||||
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
||||
return when (direction) {
|
||||
PaginationDirection.FORWARDS -> forwardsDisplayIndex
|
||||
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
|
||||
} ?: defaultValue
|
||||
PaginationDirection.FORWARDS -> forwardsDisplayIndex
|
||||
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
|
||||
} ?: defaultValue
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
||||
return when (direction) {
|
||||
PaginationDirection.FORWARDS -> forwardsStateIndex
|
||||
PaginationDirection.BACKWARDS -> backwardsStateIndex
|
||||
} ?: defaultValue
|
||||
PaginationDirection.FORWARDS -> forwardsStateIndex
|
||||
PaginationDirection.BACKWARDS -> backwardsStateIndex
|
||||
} ?: defaultValue
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
package im.vector.matrix.android.internal.database.helper
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.internal.database.mapper.toEntity
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
@ -49,6 +50,16 @@ internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
|
||||
this.stateIndex = stateIndex
|
||||
this.isUnlinked = isUnlinked
|
||||
}
|
||||
untimelinedStateEvents.add(eventEntity)
|
||||
untimelinedStateEvents.add(0, eventEntity)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun RoomEntity.addSendingEvent(event: Event,
|
||||
stateIndex: Int) {
|
||||
assertIsManaged()
|
||||
val eventEntity = event.toEntity(roomId).apply {
|
||||
this.sendState = SendState.UNSENT
|
||||
this.stateIndex = stateIndex
|
||||
}
|
||||
sendingTimelineEvents.add(0, eventEntity)
|
||||
}
|
||||
|
@ -16,12 +16,15 @@
|
||||
|
||||
package im.vector.matrix.android.internal.database.model
|
||||
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmResults
|
||||
import io.realm.annotations.Ignore
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.LinkingObjects
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import java.util.*
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(),
|
||||
@Index var eventId: String = "",
|
||||
@ -45,6 +48,13 @@ internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUI
|
||||
BOTH
|
||||
}
|
||||
|
||||
private var sendStateStr: String = SendState.UNKNOWN.name
|
||||
|
||||
@delegate:Ignore
|
||||
var sendState: SendState by Delegates.observable(SendState.valueOf(sendStateStr)) { _, _, newValue ->
|
||||
sendStateStr = newValue.name
|
||||
}
|
||||
|
||||
companion object
|
||||
|
||||
@LinkingObjects("events")
|
||||
|
@ -26,6 +26,7 @@ import kotlin.properties.Delegates
|
||||
internal open class RoomEntity(@PrimaryKey var roomId: String = "",
|
||||
var chunks: RealmList<ChunkEntity> = RealmList(),
|
||||
var untimelinedStateEvents: RealmList<EventEntity> = RealmList(),
|
||||
var sendingTimelineEvents: RealmList<EventEntity> = RealmList(),
|
||||
var areAllMembersLoaded: Boolean = false
|
||||
) : RealmObject() {
|
||||
|
||||
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2019 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.network
|
||||
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.RequestBody
|
||||
import okio.Buffer
|
||||
import okio.BufferedSink
|
||||
import okio.ForwardingSink
|
||||
import okio.Okio
|
||||
import okio.Sink
|
||||
import java.io.IOException
|
||||
|
||||
internal class ProgressRequestBody(private val delegate: RequestBody,
|
||||
private val listener: Listener) : RequestBody() {
|
||||
|
||||
private lateinit var countingSink: CountingSink
|
||||
|
||||
override fun contentType(): MediaType? {
|
||||
return delegate.contentType()
|
||||
}
|
||||
|
||||
override fun contentLength(): Long {
|
||||
try {
|
||||
return delegate.contentLength()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
countingSink = CountingSink(sink)
|
||||
val bufferedSink = Okio.buffer(countingSink)
|
||||
delegate.writeTo(bufferedSink)
|
||||
bufferedSink.flush()
|
||||
}
|
||||
|
||||
private inner class CountingSink(delegate: Sink) : ForwardingSink(delegate) {
|
||||
|
||||
private var bytesWritten: Long = 0
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(source: Buffer, byteCount: Long) {
|
||||
super.write(source, byteCount)
|
||||
bytesWritten += byteCount
|
||||
listener.onProgress(bytesWritten, contentLength())
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onProgress(current: Long, total: Long)
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.group.Group
|
||||
import im.vector.matrix.android.api.session.group.GroupService
|
||||
@ -37,6 +38,7 @@ import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.internal.database.LiveEntityObserver
|
||||
import im.vector.matrix.android.internal.di.MatrixKoinComponent
|
||||
import im.vector.matrix.android.internal.di.MatrixKoinHolder
|
||||
import im.vector.matrix.android.internal.session.content.ContentModule
|
||||
import im.vector.matrix.android.internal.session.group.GroupModule
|
||||
import im.vector.matrix.android.internal.session.room.RoomModule
|
||||
import im.vector.matrix.android.internal.session.signout.SignOutModule
|
||||
@ -64,6 +66,7 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
|
||||
private val signOutService by inject<SignOutService>()
|
||||
private val syncThread by inject<SyncThread>()
|
||||
private val contentUrlResolver by inject<ContentUrlResolver>()
|
||||
private val contentUploadProgressTracker by inject<ContentUploadStateTracker>()
|
||||
private var isOpen = false
|
||||
|
||||
@MainThread
|
||||
@ -77,7 +80,8 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
|
||||
val groupModule = GroupModule().definition
|
||||
val signOutModule = SignOutModule().definition
|
||||
val userModule = UserModule().definition
|
||||
MatrixKoinHolder.instance.loadModules(listOf(sessionModule, syncModule, roomModule, groupModule, signOutModule, userModule))
|
||||
val contentModule = ContentModule().definition
|
||||
MatrixKoinHolder.instance.loadModules(listOf(sessionModule, syncModule, roomModule, groupModule, userModule, signOutModule, contentModule))
|
||||
scope = getKoin().getOrCreateScope(SCOPE)
|
||||
if (!monarchy.isMonarchyThreadOpen) {
|
||||
monarchy.openManually()
|
||||
@ -121,6 +125,10 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
|
||||
return contentUrlResolver
|
||||
}
|
||||
|
||||
override fun contentUploadProgressTracker(): ContentUploadStateTracker {
|
||||
return contentUploadProgressTracker
|
||||
}
|
||||
|
||||
override fun addListener(listener: Session.Listener) {
|
||||
sessionListeners.addListener(listener)
|
||||
}
|
||||
|
@ -19,13 +19,11 @@ package im.vector.matrix.android.internal.session
|
||||
import android.content.Context
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.group.GroupService
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import im.vector.matrix.android.api.session.signout.SignOutService
|
||||
import im.vector.matrix.android.api.session.user.UserService
|
||||
import im.vector.matrix.android.internal.database.LiveEntityObserver
|
||||
import im.vector.matrix.android.internal.session.content.DefaultContentUrlResolver
|
||||
import im.vector.matrix.android.internal.session.group.DefaultGroupService
|
||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.room.DefaultRoomService
|
||||
@ -116,10 +114,6 @@ internal class SessionModule(private val sessionParams: SessionParams) {
|
||||
SessionListeners()
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
DefaultContentUrlResolver(sessionParams.homeServerConnectionConfig) as ContentUrlResolver
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
val groupSummaryUpdater = GroupSummaryUpdater(get())
|
||||
val eventsPruner = EventsPruner(get())
|
||||
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2019 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.content
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.internal.session.DefaultSession
|
||||
import org.koin.dsl.module.module
|
||||
|
||||
internal class ContentModule {
|
||||
|
||||
val definition = module(override = true) {
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
DefaultContentUploadStateTracker() as ContentUploadStateTracker
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
ContentUploader(get(), get(), get<ContentUploadStateTracker>() as DefaultContentUploadStateTracker)
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
val sessionParams = get<SessionParams>()
|
||||
DefaultContentUrlResolver(sessionParams.homeServerConnectionConfig) as ContentUrlResolver
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 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.content
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ContentUploadResponse(
|
||||
@Json(name = "content_uri") val contentUri: String
|
||||
)
|
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2019 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.content
|
||||
|
||||
import arrow.core.Try
|
||||
import arrow.core.Try.Companion.raise
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
internal class ContentUploader(private val okHttpClient: OkHttpClient,
|
||||
private val sessionParams: SessionParams,
|
||||
private val contentUploadProgressTracker: DefaultContentUploadStateTracker) {
|
||||
|
||||
private val moshi = MoshiProvider.providesMoshi()
|
||||
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)
|
||||
|
||||
fun uploadFile(eventId: String, attachment: ContentAttachmentData): Try<ContentUploadResponse> {
|
||||
if (attachment.path == null || attachment.mimeType == null) {
|
||||
return raise(RuntimeException())
|
||||
}
|
||||
val file = File(attachment.path)
|
||||
val urlString = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload"
|
||||
|
||||
val urlBuilder = HttpUrl.parse(urlString)?.newBuilder()
|
||||
?: return raise(RuntimeException())
|
||||
|
||||
val httpUrl = urlBuilder
|
||||
.addQueryParameter(
|
||||
"filename", attachment.name
|
||||
).build()
|
||||
|
||||
val requestBody = RequestBody.create(
|
||||
MediaType.parse(attachment.mimeType),
|
||||
file
|
||||
)
|
||||
val progressRequestBody = ProgressRequestBody(requestBody, object : ProgressRequestBody.Listener {
|
||||
override fun onProgress(current: Long, total: Long) {
|
||||
contentUploadProgressTracker.setProgress(eventId, current, total)
|
||||
}
|
||||
})
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(httpUrl)
|
||||
.post(progressRequestBody)
|
||||
.build()
|
||||
|
||||
val result = Try {
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException()
|
||||
} else {
|
||||
response.body()?.source()?.let {
|
||||
responseAdapter.fromJson(it)
|
||||
}
|
||||
?: throw IOException()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.isFailure()) {
|
||||
contentUploadProgressTracker.setFailure(eventId)
|
||||
} else {
|
||||
contentUploadProgressTracker.setSuccess(eventId)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2019 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.content
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
|
||||
internal class DefaultContentUploadStateTracker : ContentUploadStateTracker {
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val progressByEvent = mutableMapOf<String, ContentUploadStateTracker.State>()
|
||||
private val listenersByEvent = mutableMapOf<String, MutableList<ContentUploadStateTracker.UpdateListener>>()
|
||||
|
||||
override fun track(eventId: String, updateListener: ContentUploadStateTracker.UpdateListener) {
|
||||
val listeners = listenersByEvent[eventId] ?: ArrayList()
|
||||
listeners.add(updateListener)
|
||||
listenersByEvent[eventId] = listeners
|
||||
val currentState = progressByEvent[eventId] ?: ContentUploadStateTracker.State.Idle
|
||||
mainHandler.post { updateListener.onUpdate(currentState) }
|
||||
}
|
||||
|
||||
override fun untrack(eventId: String, updateListener: ContentUploadStateTracker.UpdateListener) {
|
||||
listenersByEvent[eventId]?.apply {
|
||||
remove(updateListener)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setFailure(eventId: String) {
|
||||
val failure = ContentUploadStateTracker.State.Failure
|
||||
updateState(eventId, failure)
|
||||
}
|
||||
|
||||
internal fun setSuccess(eventId: String) {
|
||||
val success = ContentUploadStateTracker.State.Success
|
||||
updateState(eventId, success)
|
||||
}
|
||||
|
||||
internal fun setProgress(eventId: String, current: Long, total: Long) {
|
||||
val progressData = ContentUploadStateTracker.State.ProgressData(current, total)
|
||||
updateState(eventId, progressData)
|
||||
}
|
||||
|
||||
private fun updateState(eventId: String, state: ContentUploadStateTracker.State) {
|
||||
progressByEvent[eventId] = state
|
||||
mainHandler.post {
|
||||
listenersByEvent[eventId]?.also { listeners ->
|
||||
listeners.forEach { it.onUpdate(state) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
|
||||
|
||||
private const val MATRIX_CONTENT_URI_SCHEME = "mxc://"
|
||||
private const val URI_PREFIX_CONTENT_API = "/_matrix/media/v1/"
|
||||
internal const val URI_PREFIX_CONTENT_API = "/_matrix/media/v1/"
|
||||
|
||||
internal class DefaultContentUrlResolver(private val homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver {
|
||||
|
||||
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2019 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.content
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
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.MessageImageContent
|
||||
import im.vector.matrix.android.internal.di.MatrixKoinComponent
|
||||
import im.vector.matrix.android.internal.session.room.send.SendEventWorker
|
||||
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||
import org.koin.standalone.inject
|
||||
|
||||
internal class UploadContentWorker(context: Context, params: WorkerParameters)
|
||||
: CoroutineWorker(context, params), MatrixKoinComponent {
|
||||
|
||||
private val mediaUploader by inject<ContentUploader>()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
val roomId: String,
|
||||
val event: Event,
|
||||
val attachment: ContentAttachmentData
|
||||
)
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.failure()
|
||||
|
||||
if (params.event.eventId == null) {
|
||||
return Result.failure()
|
||||
}
|
||||
return mediaUploader
|
||||
.uploadFile(params.event.eventId, params.attachment)
|
||||
.fold({ handleFailure() }, { handleSuccess(params, it) })
|
||||
}
|
||||
|
||||
private fun handleFailure(): Result {
|
||||
return Result.retry()
|
||||
}
|
||||
|
||||
private fun handleSuccess(params: Params, contentUploadResponse: ContentUploadResponse): Result {
|
||||
val event = updateEvent(params.event, contentUploadResponse.contentUri)
|
||||
val sendParams = SendEventWorker.Params(params.roomId, event)
|
||||
return Result.success(WorkerParamsFactory.toData(sendParams))
|
||||
}
|
||||
|
||||
private fun updateEvent(event: Event, url: String): Event {
|
||||
val messageContent: MessageContent = event.content.toModel() ?: return event
|
||||
val updatedContent = when (messageContent) {
|
||||
is MessageImageContent -> messageContent.update(url)
|
||||
else -> messageContent
|
||||
}
|
||||
return event.copy(content = updatedContent.toContent())
|
||||
}
|
||||
|
||||
private fun MessageImageContent.update(url: String): MessageImageContent {
|
||||
return copy(url = url)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.GroupEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||
|
@ -25,7 +25,7 @@ import im.vector.matrix.android.internal.session.room.members.RoomMemberExtracto
|
||||
import im.vector.matrix.android.internal.session.room.read.DefaultReadService
|
||||
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
||||
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
|
||||
import im.vector.matrix.android.internal.session.room.send.EventFactory
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||
import im.vector.matrix.android.internal.session.room.state.DefaultStateService
|
||||
import im.vector.matrix.android.internal.session.room.state.SendStateTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
|
||||
@ -41,7 +41,7 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val paginationTask: PaginationTask,
|
||||
private val contextOfEventTask: GetContextOfEventTask,
|
||||
private val setReadMarkersTask: SetReadMarkersTask,
|
||||
private val eventFactory: EventFactory,
|
||||
private val eventFactory: LocalEchoEventFactory,
|
||||
private val taskExecutor: TaskExecutor) {
|
||||
|
||||
fun instantiate(roomId: String): Room {
|
||||
|
@ -25,10 +25,14 @@ import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMem
|
||||
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
|
||||
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
||||
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
||||
import im.vector.matrix.android.internal.session.room.send.EventFactory
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
|
||||
import im.vector.matrix.android.internal.session.room.state.SendStateTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.*
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
||||
import org.koin.dsl.module.module
|
||||
import retrofit2.Retrofit
|
||||
|
||||
@ -63,7 +67,7 @@ class RoomModule {
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
EventFactory(get())
|
||||
LocalEchoEventFactory(get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
|
@ -35,7 +35,9 @@ internal class EventsPruner(monarchy: Monarchy) :
|
||||
override val query = Monarchy.Query<EventEntity> { EventEntity.where(it, type = EventType.REDACTION) }
|
||||
|
||||
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
|
||||
val redactionEvents = inserted.map { it.asDomain() }
|
||||
val redactionEvents = inserted
|
||||
.mapNotNull { it.asDomain().redacts }
|
||||
|
||||
val pruneEventWorkerParams = PruneEventWorker.Params(redactionEvents)
|
||||
val workData = WorkerParamsFactory.toData(pruneEventWorkerParams)
|
||||
|
||||
|
@ -21,7 +21,6 @@ import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
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.internal.database.mapper.ContentMapper
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
@ -37,8 +36,8 @@ internal class PruneEventWorker(context: Context,
|
||||
) : Worker(context, workerParameters), MatrixKoinComponent {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
val redactionEvents: List<Event>
|
||||
internal class Params(
|
||||
val eventIdsToRedact: List<String>
|
||||
)
|
||||
|
||||
private val monarchy by inject<Monarchy>()
|
||||
@ -48,18 +47,19 @@ internal class PruneEventWorker(context: Context,
|
||||
?: return Result.failure()
|
||||
|
||||
val result = monarchy.tryTransactionSync { realm ->
|
||||
params.redactionEvents.forEach { event ->
|
||||
pruneEvent(realm, event)
|
||||
params.eventIdsToRedact.forEach { eventId ->
|
||||
pruneEvent(realm, eventId)
|
||||
}
|
||||
}
|
||||
return result.fold({ Result.retry() }, { Result.success() })
|
||||
}
|
||||
|
||||
private fun pruneEvent(realm: Realm, redactionEvent: Event?) {
|
||||
if (redactionEvent == null || redactionEvent.redacts.isNullOrEmpty()) {
|
||||
private fun pruneEvent(realm: Realm, eventIdToRedact: String) {
|
||||
if (eventIdToRedact.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
|
||||
|
||||
val eventToPrune = EventEntity.where(realm, eventId = eventIdToRedact).findFirst()
|
||||
?: return
|
||||
|
||||
val allowedKeys = computeAllowedKeys(eventToPrune.type)
|
||||
@ -87,7 +87,7 @@ internal class PruneEventWorker(context: Context,
|
||||
EventType.STATE_ROOM_ALIASES -> listOf("aliases")
|
||||
EventType.STATE_CANONICAL_ALIAS -> listOf("alias")
|
||||
EventType.FEEDBACK -> listOf("type", "target_event_id")
|
||||
else -> emptyList()
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,56 +16,115 @@
|
||||
|
||||
package im.vector.matrix.android.internal.session.room.send
|
||||
|
||||
import androidx.work.*
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.send.SendService
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.database.helper.add
|
||||
import im.vector.matrix.android.api.util.CancelableBag
|
||||
import im.vector.matrix.android.api.util.addTo
|
||||
import im.vector.matrix.android.internal.database.helper.addSendingEvent
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.content.UploadContentWorker
|
||||
import im.vector.matrix.android.internal.util.CancelableWork
|
||||
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val SEND_WORK = "SEND_WORK"
|
||||
private const val UPLOAD_WORK = "UPLOAD_WORK"
|
||||
private const val BACKOFF_DELAY = 10_000L
|
||||
|
||||
private val WORK_CONSTRAINTS = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
internal class DefaultSendService(private val roomId: String,
|
||||
private val eventFactory: EventFactory,
|
||||
private val monarchy: Monarchy) : SendService {
|
||||
private val eventFactory: LocalEchoEventFactory,
|
||||
private val monarchy: Monarchy)
|
||||
: SendService {
|
||||
|
||||
private val sendConstraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
// TODO callback is not used
|
||||
override fun sendTextMessage(text: String, msgType: String, callback: MatrixCallback<Event>): Cancelable {
|
||||
val event = eventFactory.createTextEvent(roomId, msgType, text)
|
||||
|
||||
monarchy.tryTransactionAsync { realm ->
|
||||
val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||
?: return@tryTransactionAsync
|
||||
chunkEntity.add(roomId, event, PaginationDirection.FORWARDS)
|
||||
override fun sendTextMessage(text: String, msgType: String): Cancelable {
|
||||
val event = eventFactory.createTextEvent(roomId, msgType, text).also {
|
||||
saveLocalEcho(it)
|
||||
}
|
||||
val sendWork = createSendEventWork(event)
|
||||
WorkManager.getInstance()
|
||||
.beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork)
|
||||
.enqueue()
|
||||
return CancelableWork(sendWork.id)
|
||||
}
|
||||
|
||||
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
||||
val workData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
override fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable {
|
||||
val cancelableBag = CancelableBag()
|
||||
attachments.forEach {
|
||||
sendMedia(it).addTo(cancelableBag)
|
||||
}
|
||||
return cancelableBag
|
||||
}
|
||||
|
||||
val sendWork = OneTimeWorkRequestBuilder<SendEventWorker>()
|
||||
.setConstraints(sendConstraints)
|
||||
.setInputData(workData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
|
||||
// Create an event with the media file path
|
||||
val event = eventFactory.createMediaEvent(roomId, attachment).also {
|
||||
saveLocalEcho(it)
|
||||
}
|
||||
val uploadWork = createUploadMediaWork(event, attachment)
|
||||
val sendWork = createSendEventWork(event)
|
||||
|
||||
WorkManager.getInstance()
|
||||
.beginUniqueWork(SEND_WORK, ExistingWorkPolicy.APPEND, sendWork)
|
||||
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
||||
.then(sendWork)
|
||||
.enqueue()
|
||||
|
||||
return CancelableWork(sendWork.id)
|
||||
}
|
||||
|
||||
private fun saveLocalEcho(event: Event) {
|
||||
monarchy.tryTransactionAsync { realm ->
|
||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
||||
?: return@tryTransactionAsync
|
||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId)
|
||||
?: return@tryTransactionAsync
|
||||
|
||||
roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildWorkIdentifier(identifier: String): String {
|
||||
return "${roomId}_$identifier"
|
||||
}
|
||||
|
||||
private fun createSendEventWork(event: Event): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
return OneTimeWorkRequestBuilder<SendEventWorker>()
|
||||
.setConstraints(WORK_CONSTRAINTS)
|
||||
.setInputData(sendWorkData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest {
|
||||
val uploadMediaWorkerParams = UploadContentWorker.Params(roomId, event, attachment)
|
||||
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
|
||||
|
||||
return OneTimeWorkRequestBuilder<UploadContentWorker>()
|
||||
.setConstraints(WORK_CONSTRAINTS)
|
||||
.setInputData(uploadWorkData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2019 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.send
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
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.toContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.AudioInfo
|
||||
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.MessageFileContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.VideoInfo
|
||||
|
||||
internal class LocalEchoEventFactory(private val credentials: Credentials) {
|
||||
|
||||
fun createTextEvent(roomId: String, msgType: String, text: String): Event {
|
||||
val content = MessageTextContent(type = msgType, body = text)
|
||||
return createEvent(roomId, content)
|
||||
}
|
||||
|
||||
fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||
return when (attachment.type) {
|
||||
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment)
|
||||
ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment)
|
||||
ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment)
|
||||
ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createImageEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||
val content = MessageImageContent(
|
||||
type = MessageType.MSGTYPE_IMAGE,
|
||||
body = attachment.name ?: "image",
|
||||
info = ImageInfo(
|
||||
mimeType = attachment.mimeType ?: "image/png",
|
||||
width = attachment.width?.toInt() ?: 0,
|
||||
height = attachment.height?.toInt() ?: 0,
|
||||
size = attachment.size.toInt()
|
||||
),
|
||||
url = attachment.path
|
||||
)
|
||||
return createEvent(roomId, content)
|
||||
}
|
||||
|
||||
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||
val content = MessageVideoContent(
|
||||
type = MessageType.MSGTYPE_VIDEO,
|
||||
body = attachment.name ?: "video",
|
||||
info = VideoInfo(
|
||||
mimeType = attachment.mimeType ?: "video/mpeg",
|
||||
width = attachment.width?.toInt() ?: 0,
|
||||
height = attachment.height?.toInt() ?: 0,
|
||||
size = attachment.size,
|
||||
duration = attachment.duration?.toInt() ?: 0
|
||||
),
|
||||
url = attachment.path
|
||||
)
|
||||
return createEvent(roomId, content)
|
||||
}
|
||||
|
||||
private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||
val content = MessageAudioContent(
|
||||
type = MessageType.MSGTYPE_AUDIO,
|
||||
body = attachment.name ?: "audio",
|
||||
info = AudioInfo(
|
||||
mimeType = attachment.mimeType ?: "audio/mpeg",
|
||||
size = attachment.size
|
||||
),
|
||||
url = attachment.path
|
||||
)
|
||||
return createEvent(roomId, content)
|
||||
}
|
||||
|
||||
private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||
val content = MessageFileContent(
|
||||
type = MessageType.MSGTYPE_FILE,
|
||||
body = attachment.name ?: "file",
|
||||
info = FileInfo(
|
||||
mimeType = attachment.mimeType ?: "application/octet-stream",
|
||||
size = attachment.size
|
||||
),
|
||||
url = attachment.path
|
||||
)
|
||||
return createEvent(roomId, content)
|
||||
}
|
||||
|
||||
private fun createEvent(roomId: String, content: Any? = null): Event {
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
originServerTs = dummyOriginServerTs(),
|
||||
sender = credentials.userId,
|
||||
eventId = dummyEventId(roomId),
|
||||
type = EventType.MESSAGE,
|
||||
content = content.toContent()
|
||||
)
|
||||
}
|
||||
|
||||
private fun dummyOriginServerTs(): Long {
|
||||
return System.currentTimeMillis()
|
||||
}
|
||||
|
||||
private fun dummyEventId(roomId: String): String {
|
||||
return roomId + "-" + dummyOriginServerTs()
|
||||
}
|
||||
}
|
@ -20,15 +20,11 @@ import android.content.Context
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.MatrixKoinComponent
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||
import org.koin.standalone.inject
|
||||
|
||||
internal class SendEventWorker(context: Context, params: WorkerParameters)
|
||||
@ -42,33 +38,25 @@ internal class SendEventWorker(context: Context, params: WorkerParameters)
|
||||
)
|
||||
|
||||
private val roomAPI by inject<RoomAPI>()
|
||||
private val monarchy by inject<Monarchy>()
|
||||
|
||||
override fun doWork(): Result {
|
||||
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.failure()
|
||||
|
||||
if (params.event.eventId == null) {
|
||||
val localEvent = params.event
|
||||
if (localEvent.eventId == null) {
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val result = executeRequest<SendResponse> {
|
||||
apiCall = roomAPI.send(
|
||||
params.event.eventId,
|
||||
localEvent.eventId,
|
||||
params.roomId,
|
||||
params.event.type,
|
||||
params.event.content
|
||||
localEvent.type,
|
||||
localEvent.content
|
||||
)
|
||||
}
|
||||
result.flatMap { sendResponse ->
|
||||
monarchy.tryTransactionSync { realm ->
|
||||
val dummyEventEntity = EventEntity.where(realm, params.event.eventId).findFirst()
|
||||
dummyEventEntity?.eventId = sendResponse.eventId
|
||||
}
|
||||
}
|
||||
return result.fold({ Result.retry() }, { Result.success() })
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -27,25 +27,19 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.util.CancelableBag
|
||||
import im.vector.matrix.android.api.util.addTo
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
||||
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.model.*
|
||||
import im.vector.matrix.android.internal.database.query.findIncludingEvent
|
||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import io.realm.Sort
|
||||
import im.vector.matrix.android.internal.util.Debouncer
|
||||
import io.realm.*
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
private const val INITIAL_LOAD_SIZE = 20
|
||||
@ -68,8 +62,7 @@ internal class DefaultTimeline(
|
||||
set(value) {
|
||||
field = value
|
||||
backgroundHandler.get()?.post {
|
||||
val snapshot = snapshot()
|
||||
mainHandler.post { listener?.onUpdated(snapshot) }
|
||||
postSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,41 +73,46 @@ internal class DefaultTimeline(
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val backgroundRealm = AtomicReference<Realm>()
|
||||
private val cancelableBag = CancelableBag()
|
||||
private val debouncer = Debouncer(mainHandler)
|
||||
|
||||
private lateinit var liveEvents: RealmResults<EventEntity>
|
||||
private var roomEntity: RoomEntity? = null
|
||||
|
||||
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
||||
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
||||
private val isLive = initialEventId == null
|
||||
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||
|
||||
private val backwardsPaginationState = AtomicReference(PaginationState())
|
||||
private val forwardsPaginationState = AtomicReference(PaginationState())
|
||||
|
||||
|
||||
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
|
||||
// TODO HANDLE CHANGES
|
||||
changeSet.insertionRanges.forEach { range ->
|
||||
val (startDisplayIndex, direction) = if (range.startIndex == 0) {
|
||||
Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS)
|
||||
} else {
|
||||
Pair(liveEvents[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS)
|
||||
}
|
||||
val state = getPaginationState(direction)
|
||||
if (state.isPaginating) {
|
||||
// We are getting new items from pagination
|
||||
val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedCount)
|
||||
if (shouldPostSnapshot) {
|
||||
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) {
|
||||
handleInitialLoad()
|
||||
} else {
|
||||
changeSet.insertionRanges.forEach { range ->
|
||||
val (startDisplayIndex, direction) = if (range.startIndex == 0) {
|
||||
Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS)
|
||||
} else {
|
||||
Pair(liveEvents[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS)
|
||||
}
|
||||
val state = getPaginationState(direction)
|
||||
if (state.isPaginating) {
|
||||
// We are getting new items from pagination
|
||||
val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedCount)
|
||||
if (shouldPostSnapshot) {
|
||||
postSnapshot()
|
||||
}
|
||||
} else {
|
||||
// We are getting new items from sync
|
||||
buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
|
||||
postSnapshot()
|
||||
}
|
||||
} else {
|
||||
// We are getting new items from sync
|
||||
buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
|
||||
postSnapshot()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public methods ******************************************************************************
|
||||
// Public methods ******************************************************************************
|
||||
|
||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||
backgroundHandler.get()?.post {
|
||||
@ -142,12 +140,19 @@ internal class DefaultTimeline(
|
||||
val realm = Realm.getInstance(realmConfiguration)
|
||||
backgroundRealm.set(realm)
|
||||
clearUnlinkedEvents(realm)
|
||||
isReady.set(true)
|
||||
|
||||
roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()?.also {
|
||||
it.sendingTimelineEvents.addChangeListener { _ ->
|
||||
postSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
liveEvents = buildEventQuery(realm)
|
||||
.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
.findAll()
|
||||
.findAllAsync()
|
||||
.also { it.addChangeListener(eventsChangeListener) }
|
||||
handleInitialLoad()
|
||||
|
||||
isReady.set(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -171,7 +176,7 @@ internal class DefaultTimeline(
|
||||
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
||||
}
|
||||
|
||||
// Private methods *****************************************************************************
|
||||
// Private methods *****************************************************************************
|
||||
|
||||
private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
|
||||
val localRealm = Realm.getInstance(realmConfiguration)
|
||||
@ -203,7 +208,7 @@ internal class DefaultTimeline(
|
||||
|
||||
/**
|
||||
* This has to be called on TimelineThread as it access realm live results
|
||||
* @return true if snapshot should be posted
|
||||
* @return true if createSnapshot should be posted
|
||||
*/
|
||||
private fun paginateInternal(startDisplayIndex: Int,
|
||||
direction: Timeline.Direction,
|
||||
@ -222,8 +227,19 @@ internal class DefaultTimeline(
|
||||
return !shouldFetchMore
|
||||
}
|
||||
|
||||
private fun snapshot(): List<TimelineEvent> {
|
||||
return builtEvents.toList()
|
||||
private fun createSnapshot(): List<TimelineEvent> {
|
||||
return buildSendingEvents() + builtEvents.toList()
|
||||
}
|
||||
|
||||
private fun buildSendingEvents(): List<TimelineEvent> {
|
||||
val sendingEvents = ArrayList<TimelineEvent>()
|
||||
if (hasReachedEnd(Timeline.Direction.FORWARDS)) {
|
||||
roomEntity?.sendingTimelineEvents?.forEach {
|
||||
val timelineEvent = timelineEventFactory.create(it)
|
||||
sendingEvents.add(timelineEvent)
|
||||
}
|
||||
}
|
||||
return sendingEvents
|
||||
}
|
||||
|
||||
private fun canPaginate(direction: Timeline.Direction): Boolean {
|
||||
@ -282,9 +298,9 @@ internal class DefaultTimeline(
|
||||
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
|
||||
val token = getTokenLive(direction) ?: return
|
||||
val params = PaginationTask.Params(roomId = roomId,
|
||||
from = token,
|
||||
direction = direction.toPaginationDirection(),
|
||||
limit = limit)
|
||||
from = token,
|
||||
direction = direction.toPaginationDirection(),
|
||||
limit = limit)
|
||||
|
||||
Timber.v("Should fetch $limit items $direction")
|
||||
paginationTask.configureWith(params)
|
||||
@ -414,8 +430,9 @@ internal class DefaultTimeline(
|
||||
}
|
||||
|
||||
private fun postSnapshot() {
|
||||
val snapshot = snapshot()
|
||||
mainHandler.post { listener?.onUpdated(snapshot) }
|
||||
val snapshot = createSnapshot()
|
||||
val runnable = Runnable { listener?.onUpdated(snapshot) }
|
||||
debouncer.debounce("post_snapshot", runnable, 50)
|
||||
}
|
||||
|
||||
// Extension methods ***************************************************************************
|
||||
|
@ -29,7 +29,8 @@ internal class TimelineEventFactory(private val roomMemberExtractor: RoomMemberE
|
||||
eventEntity.asDomain(),
|
||||
eventEntity.localId,
|
||||
eventEntity.displayIndex,
|
||||
roomMember
|
||||
roomMember,
|
||||
eventEntity.sendState
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ import im.vector.matrix.android.internal.database.helper.addStateEvents
|
||||
import im.vector.matrix.android.internal.database.helper.lastStateIndex
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
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.where
|
||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||
@ -108,6 +109,15 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
||||
timelineStateOffset
|
||||
)
|
||||
roomEntity.addOrUpdate(chunkEntity)
|
||||
|
||||
// Try to remove local echo
|
||||
val transactionIds = roomSync.timeline.events.mapNotNull { it.unsignedData?.transactionId }
|
||||
transactionIds.forEach {
|
||||
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
||||
if (sendingEventEntity != null) {
|
||||
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
roomSummaryUpdater.update(realm, roomId, roomSync.summary, roomSync.unreadNotifications)
|
||||
|
||||
@ -161,7 +171,6 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
||||
} else {
|
||||
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
|
||||
}
|
||||
|
||||
lastChunk?.isLastForward = false
|
||||
chunkEntity.isLastForward = true
|
||||
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset)
|
||||
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 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.util
|
||||
|
||||
import android.os.Handler
|
||||
|
||||
internal class Debouncer(private val handler: Handler) {
|
||||
|
||||
private val runnables = HashMap<String, Runnable>()
|
||||
|
||||
fun debounce(identifier: String, r: Runnable, millis: Long): Boolean {
|
||||
if (runnables.containsKey(identifier)) {
|
||||
// debounce
|
||||
val old = runnables[identifier]
|
||||
handler.removeCallbacks(old)
|
||||
}
|
||||
insertRunnable(identifier, r, millis)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun insertRunnable(identifier: String, r: Runnable, millis: Long) {
|
||||
val chained = Runnable {
|
||||
handler.post(r)
|
||||
runnables.remove(identifier)
|
||||
}
|
||||
runnables[identifier] = chained
|
||||
handler.postDelayed(chained, millis)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user