diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index e3b9ba732a..c04907abff 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 5c09ac8a07..68fc07a8eb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -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. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt new file mode 100644 index 0000000000..82a4a762ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt @@ -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 + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt new file mode 100644 index 0000000000..0d88e5faf7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt @@ -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() + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/VideoInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/VideoInfo.kt index 1a0768df56..701a62728b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/VideoInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/VideoInfo.kt @@ -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, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index 7ab9495b68..6a0d672ce6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -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): 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): Cancelable } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt new file mode 100644 index 0000000000..b5b2264e85 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt @@ -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 + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index ffd7dbc65c..ca971b653d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -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() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt new file mode 100644 index 0000000000..8db44148d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt @@ -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(private val realmConfiguration: RealmConfiguration, + private val realmQueryBuilder: (Realm) -> RealmQuery) { + + 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> { + override fun onChange(t: RealmResults) { + if (t.isNotEmpty()) { + result.removeChangeListener(this) + realm.close() + latch.countDown() + } + } + }) + } + handler.post(runnable) + latch.await() + handlerThread.quit() + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 850fd43137..6df95e107f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -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 } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt index dedce53970..7cb6124c0b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt @@ -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, 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) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index c688a11c43..c2fa36c17b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -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") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt index e9293c0ee6..6c61d0de69 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt @@ -26,6 +26,7 @@ import kotlin.properties.Delegates internal open class RoomEntity(@PrimaryKey var roomId: String = "", var chunks: RealmList = RealmList(), var untimelinedStateEvents: RealmList = RealmList(), + var sendingTimelineEvents: RealmList = RealmList(), var areAllMembersLoaded: Boolean = false ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt new file mode 100644 index 0000000000..fabd9763e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt @@ -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) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index a0ccaa9641..1c4e12f860 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -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() private val syncThread by inject() private val contentUrlResolver by inject() + private val contentUploadProgressTracker by inject() 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) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 5c18c66adc..76a55a4bf4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -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()) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt new file mode 100644 index 0000000000..b149cd3867 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt @@ -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() as DefaultContentUploadStateTracker) + } + + scope(DefaultSession.SCOPE) { + val sessionParams = get() + DefaultContentUrlResolver(sessionParams.homeServerConnectionConfig) as ContentUrlResolver + } + + } + + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploadResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploadResponse.kt new file mode 100644 index 0000000000..e1f456fe5f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploadResponse.kt @@ -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 +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploader.kt new file mode 100644 index 0000000000..42dff4f036 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploader.kt @@ -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 { + 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 + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt new file mode 100644 index 0000000000..1e6ca3c1c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt @@ -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() + private val listenersByEvent = mutableMapOf>() + + 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) } + } + } + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt index c9b71fdbf7..cb8896e777 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt @@ -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 { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt new file mode 100644 index 0000000000..f864e725ff --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -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() + + @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(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) + } + + +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt index d756b73c7a..826c53fcdd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index a92714f4d6..146794e15d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -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 { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 78cf1eed8d..1732fe691b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -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) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt index e4bf77706b..ad17032ae3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt @@ -35,7 +35,9 @@ internal class EventsPruner(monarchy: Monarchy) : override val query = Monarchy.Query { EventEntity.where(it, type = EventType.REDACTION) } override fun processChanges(inserted: List, updated: List, deleted: List) { - val redactionEvents = inserted.map { it.asDomain() } + val redactionEvents = inserted + .mapNotNull { it.asDomain().redacts } + val pruneEventWorkerParams = PruneEventWorker.Params(redactionEvents) val workData = WorkerParamsFactory.toData(pruneEventWorkerParams) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt index 786218a1b6..245aa551f7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt @@ -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 + internal class Params( + val eventIdsToRedact: List ) private val monarchy by inject() @@ -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() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 4274951161..0089844c45 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -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): 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): Cancelable { + val cancelableBag = CancelableBag() + attachments.forEach { + sendMedia(it).addTo(cancelableBag) + } + return cancelableBag + } - val sendWork = OneTimeWorkRequestBuilder() - .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() + .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() + .setConstraints(WORK_CONSTRAINTS) + .setInputData(uploadWorkData) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() } } 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 new file mode 100644 index 0000000000..56051503fc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -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() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index bbe8455e62..864ef13e0b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -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() - private val monarchy by inject() override fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() - if (params.event.eventId == null) { + val localEvent = params.event + if (localEvent.eventId == null) { return Result.failure() } val result = executeRequest { 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() }) } - - } 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 38555d2027..89a8307b7f 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 @@ -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() private val cancelableBag = CancelableBag() + private val debouncer = Debouncer(mainHandler) private lateinit var liveEvents: RealmResults + 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(ArrayList()) - private val backwardsPaginationState = AtomicReference(PaginationState()) private val forwardsPaginationState = AtomicReference(PaginationState()) + private val eventsChangeListener = OrderedRealmCollectionChangeListener> { _, 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 { - return builtEvents.toList() + private fun createSnapshot(): List { + return buildSendingEvents() + builtEvents.toList() + } + + private fun buildSendingEvents(): List { + val sendingEvents = ArrayList() + 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 *************************************************************************** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt index d748fe7fc7..20c0ebee68 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt @@ -29,7 +29,8 @@ internal class TimelineEventFactory(private val roomMemberExtractor: RoomMemberE eventEntity.asDomain(), eventEntity.localId, eventEntity.displayIndex, - roomMember + roomMember, + eventEntity.sendState ) } 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 5692961863..24dff3398a 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 @@ -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().apply { this.prevToken = prevToken } } - lastChunk?.isLastForward = false chunkEntity.isLastForward = true chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Debouncer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Debouncer.kt new file mode 100644 index 0000000000..2f9921b98f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Debouncer.kt @@ -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() + + 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) + } +} \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index bb99d9aca0..691090f07f 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -123,7 +123,7 @@ dependencies { def coroutines_version = "1.0.1" def markwon_version = '3.0.0-SNAPSHOT' def big_image_viewer_version = '1.5.6' - def glide_version = '4.8.0' + def glide_version = '4.9.0' implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") @@ -191,6 +191,9 @@ dependencies { // Badge for compatibility implementation 'me.leolin:ShortcutBadger:1.1.2@aar' + // File picker + implementation 'com.github.jaiselrahman:FilePicker:1.2.2' + // DI implementation "org.koin:koin-android:$koin_version" implementation "org.koin:koin-android-scope:$koin_version" diff --git a/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogAdapter.kt b/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogAdapter.kt new file mode 100644 index 0000000000..5f51350e05 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogAdapter.kt @@ -0,0 +1,41 @@ +/* + * 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.riotredesign.core.dialogs + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import im.vector.riotredesign.R + +internal abstract class DialogAdapter(context: Context) : ArrayAdapter(context, R.layout.item_dialog) { + + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { + var view = convertView + if (view == null) { + view = LayoutInflater.from(context).inflate(R.layout.item_dialog, parent, false) + view.tag = DialogListItemHolder(view) + } + (view!!.tag as DialogListItemHolder).let { + it.icon.setImageResource(getItem(position).iconRes) + it.text.setText(getItem(position).titleRes) + } + return view + } + +} diff --git a/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogCallAdapter.kt b/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogCallAdapter.kt new file mode 100644 index 0000000000..5f0a954dce --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogCallAdapter.kt @@ -0,0 +1,29 @@ +/* + * 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.riotredesign.core.dialogs + +import android.content.Context +import im.vector.riotredesign.core.dialogs.DialogAdapter +import im.vector.riotredesign.core.dialogs.DialogListItem + +internal class DialogCallAdapter(context: Context) : DialogAdapter(context) { + + init { + add(DialogListItem.StartVoiceCall) + add(DialogListItem.StartVideoCall) + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogListItem.kt b/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogListItem.kt new file mode 100644 index 0000000000..d070b526a0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogListItem.kt @@ -0,0 +1,36 @@ +/* + * 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.riotredesign.core.dialogs + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import im.vector.riotredesign.R + +internal sealed class DialogListItem(@DrawableRes val iconRes: Int, + @StringRes val titleRes: Int) { + + object StartVoiceCall : DialogListItem(R.drawable.voice_call_green, R.string.action_voice_call) + object StartVideoCall : DialogListItem(R.drawable.video_call_green, R.string.action_video_call) + + object SendFile : DialogListItem(R.drawable.ic_material_file, R.string.option_send_files) + object SendVoice : DialogListItem(R.drawable.vector_micro_green, R.string.option_send_voice) + object SendSticker : DialogListItem(R.drawable.ic_send_sticker, R.string.option_send_sticker) + object TakePhoto : DialogListItem(R.drawable.ic_material_camera, R.string.option_take_photo) + object TakeVideo : DialogListItem(R.drawable.ic_material_videocam, R.string.option_take_video) + object TakePhotoVideo : DialogListItem(R.drawable.ic_material_camera, R.string.option_take_photo_video) + +} diff --git a/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogListItemHolder.kt b/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogListItemHolder.kt new file mode 100644 index 0000000000..f6ec5522b5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogListItemHolder.kt @@ -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.riotredesign.core.dialogs + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.riotredesign.R + +class DialogListItemHolder(view: View) { + + @BindView(R.id.adapter_item_dialog_icon) + lateinit var icon: ImageView + + @BindView(R.id.adapter_item_dialog_text) + lateinit var text: TextView + + init { + ButterKnife.bind(this, view) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogLocker.kt b/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogLocker.kt new file mode 100644 index 0000000000..19e75ce063 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogLocker.kt @@ -0,0 +1,66 @@ +/* + * 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.riotredesign.core.dialogs + +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import im.vector.riotredesign.core.platform.Restorable +import timber.log.Timber + +private const val KEY_DIALOG_IS_DISPLAYED = "DialogLocker.KEY_DIALOG_IS_DISPLAYED" + +/** + * Class to avoid displaying twice the same dialog + */ +class DialogLocker() : Restorable { + + private var isDialogDisplayed: Boolean = false + + private fun unlock() { + isDialogDisplayed = false + } + + private fun lock() { + isDialogDisplayed = true + } + + fun displayDialog(builder: () -> AlertDialog.Builder): AlertDialog? { + return if (isDialogDisplayed) { + Timber.w("Filtered dialog request") + null + } else { + builder + .invoke() + .create() + .apply { + setOnShowListener { lock() } + setOnCancelListener { unlock() } + setOnDismissListener { unlock() } + show() + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(KEY_DIALOG_IS_DISPLAYED, isDialogDisplayed) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle?) { + isDialogDisplayed = savedInstanceState?.getBoolean(KEY_DIALOG_IS_DISPLAYED, false) == true + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogSendItemAdapter.kt b/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogSendItemAdapter.kt new file mode 100644 index 0000000000..e1acddea9e --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/dialogs/DialogSendItemAdapter.kt @@ -0,0 +1,28 @@ +/* + * 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.riotredesign.core.dialogs + +import android.content.Context +import im.vector.riotredesign.core.dialogs.DialogAdapter +import im.vector.riotredesign.core.dialogs.DialogListItem + +internal class DialogSendItemAdapter(context: Context, items: MutableList) : DialogAdapter(context) { + + init { + addAll(items) + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/core/dialogs/ExportKeysDialog.kt b/vector/src/main/java/im/vector/riotredesign/core/dialogs/ExportKeysDialog.kt new file mode 100644 index 0000000000..90c12de42b --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/dialogs/ExportKeysDialog.kt @@ -0,0 +1,84 @@ +/* + * 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.riotredesign.core.dialogs + +import android.app.Activity +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import android.widget.Button +import androidx.appcompat.app.AlertDialog +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import im.vector.riotredesign.R + +class ExportKeysDialog { + + fun show(activity: Activity, exportKeyDialogListener: ExportKeyDialogListener) { + val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_export_e2e_keys, null) + val builder = AlertDialog.Builder(activity) + .setTitle(R.string.encryption_export_room_keys) + .setView(dialogLayout) + + val passPhrase1EditText = dialogLayout.findViewById(R.id.dialog_e2e_keys_passphrase_edit_text) + val passPhrase2EditText = dialogLayout.findViewById(R.id.dialog_e2e_keys_confirm_passphrase_edit_text) + val passPhrase2Til = dialogLayout.findViewById(R.id.dialog_e2e_keys_confirm_passphrase_til) + val exportButton = dialogLayout.findViewById