Merge branch 'develop' into cross_signing

This commit is contained in:
Valere 2019-12-30 14:27:47 +01:00
commit 0997d9abf4
172 changed files with 3470 additions and 1769 deletions

View file

@ -1,4 +1,28 @@
Changes in RiotX 0.11.0 (2019-XX-XX)
Changes in RiotX 0.12.0 (2019-XX-XX)
===================================================
Features ✨:
-
Improvements 🙌:
- The initial sync is now handled by a foreground service
- Render aliases and canonical alias change in the timeline
- Fix autocompletion issues and add support for rooms and groups
Other changes:
-
Bugfix 🐛:
- Fix avatar image disappearing (#777)
- Fix read marker banner when permalink
Translations 🗣:
-
Build 🧱:
-
Changes in RiotX 0.11.0 (2019-12-19)
===================================================
Features ✨:
@ -7,6 +31,7 @@ Features ✨:
Improvements 🙌:
- Handle navigation to room via room alias (#201)
- Open matrix.to link in RiotX (#57)
- Limit sticker size in the timeline
Other changes:
- Use same default room colors than Riot-Web
@ -14,10 +39,9 @@ Other changes:
Bugfix 🐛:
- Scroll breadcrumbs to top when opened
- Render default room name when it starts with an emoji (#477)
- Do not display " (IRC)") in display names https://github.com/vector-im/riot-android/issues/444
Translations 🗣:
-
- Do not display " (IRC)" in display names https://github.com/vector-im/riot-android/issues/444
- Fix rendering issue with HTML formatted body
- Disable click on Stickers (#703)
Build 🧱:
- Include diff-match-patch sources as dependency

View file

@ -21,5 +21,5 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, Main,
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main,
Executors.newSingleThreadExecutor().asCoroutineDispatcher())

View file

@ -1,60 +0,0 @@
/*
* 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.auth
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.OkReplayRuleChainNoActivity
import im.vector.matrix.android.api.auth.AuthenticationService
import okreplay.*
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
internal class AuthenticationServiceTest : InstrumentedTest {
lateinit var authenticationService: AuthenticationService
lateinit var okReplayInterceptor: OkReplayInterceptor
private val okReplayConfig = OkReplayConfig.Builder()
.tapeRoot(AndroidTapeRoot(
context(), javaClass))
.defaultMode(TapeMode.READ_WRITE) // or TapeMode.READ_ONLY
.sslEnabled(true)
.interceptor(okReplayInterceptor)
.build()
@get:Rule
val testRule = OkReplayRuleChainNoActivity(okReplayConfig).get()
@Test
@UiThreadTest
@OkReplay(tape = "auth", mode = TapeMode.READ_WRITE)
fun auth() {
}
companion object {
@ClassRule
@JvmField
val grantExternalStoragePermissionRule: GrantPermissionRule =
GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}

View file

@ -16,20 +16,31 @@
package im.vector.matrix.android.internal.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import io.realm.Realm
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmManager
import org.matrix.olm.OlmSession
private const val DUMMY_DEVICE_KEY = "DeviceKey"
class CryptoStoreTest {
@RunWith(AndroidJUnit4::class)
class CryptoStoreTest : InstrumentedTest {
private val cryptoStoreHelper = CryptoStoreHelper()
@Before
fun setup() {
Realm.init(context())
}
@Test
fun test_metadata_realm_ok() {
val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()

View file

@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.internal.database.helper.*
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent
@ -43,7 +44,11 @@ internal class ChunkEntityTest : InstrumentedTest {
@Before
fun setup() {
Realm.init(context())
val testConfig = RealmConfiguration.Builder().inMemory().name("test-realm").build()
val testConfig = RealmConfiguration.Builder()
.inMemory()
.name("test-realm")
.modules(SessionRealmModule())
.build()
monarchy = Monarchy.Builder().setRealmConfiguration(testConfig).build()
}

View file

@ -66,7 +66,7 @@ internal class TimelineTest : InstrumentedTest {
// val latch = CountDownLatch(2)
// var timelineEvents: List<TimelineEvent> = emptyList()
// timeline.listener = object : Timeline.Listener {
// override fun onUpdated(snapshot: List<TimelineEvent>) {
// override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
// if (snapshot.isNotEmpty()) {
// if (initialLoad == 0) {
// initialLoad = snapshot.size

View file

@ -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.matrix.android.api.failure
import javax.net.ssl.HttpsURLConnection
fun Throwable.is401() =
this is Failure.ServerError
&& httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& error.code == MatrixError.M_UNAUTHORIZED
fun Throwable.isTokenError() =
this is Failure.ServerError
&& (error.code == MatrixError.M_UNKNOWN_TOKEN || error.code == MatrixError.M_MISSING_TOKEN)

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.api.permalinks
import android.text.Spannable
import im.vector.matrix.android.api.MatrixPatterns
/**
* MatrixLinkify take a piece of text and turns all of the
@ -30,7 +29,13 @@ object MatrixLinkify {
*
* @param spannable the text in which the matrix items has to be clickable.
*/
@Suppress("UNUSED_PARAMETER")
fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean {
/**
* I disable it because it mess up with pills, and even with pills, it does not work correctly:
* The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to
*/
/*
// sanity checks
if (spannable.isEmpty()) {
return false
@ -50,5 +55,7 @@ object MatrixLinkify {
}
}
return hasMatch
*/
return false
}
}

View file

@ -56,23 +56,23 @@ object PermalinkParser {
val identifier = params.getOrNull(0)
val extraParameter = params.getOrNull(1)
if (identifier.isNullOrEmpty()) {
return PermalinkData.FallbackLink(uri)
}
return when {
identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri)
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
MatrixPatterns.isRoomId(identifier) -> {
val eventId = extraParameter.takeIf {
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it)
}
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = false, eventId = eventId)
PermalinkData.RoomLink(
roomIdOrAlias = identifier,
isRoomAlias = false,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }
)
}
MatrixPatterns.isRoomAlias(identifier) -> {
val eventId = extraParameter.takeIf {
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it)
}
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = true, eventId = eventId)
PermalinkData.RoomLink(
roomIdOrAlias = identifier,
isRoomAlias = true,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }
)
}
else -> PermalinkData.FallbackLink(uri)
}

View file

@ -109,6 +109,11 @@ interface Session :
*/
fun syncState(): LiveData<SyncState>
/**
* This methods return true if an initial sync has been processed
*/
fun hasAlreadySynced(): Boolean
/**
* This method allow to close a session. It does stop some services.
*/

View file

@ -24,7 +24,7 @@ import im.vector.matrix.android.api.MatrixCallback
interface CacheService {
/**
* Clear the whole cached data, except credentials. Once done, the session is closed and has to be opened again
* Clear the whole cached data, except credentials. Once done, the sync has to be restarted by the sdk user.
*/
fun clearCache(callback: MatrixCallback<Unit>)
}

View file

@ -30,7 +30,7 @@ data class ContentAttachmentData(
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
val name: String? = null,
val path: String,
val mimeType: String,
val mimeType: String?,
val type: Type
) : Parcelable {

View file

@ -50,10 +50,10 @@ object EventType {
const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
const val STATE_ROOM_ALIASES = "m.room.aliases"
const val STATE_ROOM_TOMBSTONE = "m.room.tombstone"
const val STATE_CANONICAL_ALIAS = "m.room.canonical_alias"
const val STATE_HISTORY_VISIBILITY = "m.room.history_visibility"
const val STATE_RELATED_GROUPS = "m.room.related_groups"
const val STATE_PINNED_EVENT = "m.room.pinned_events"
const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias"
const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility"
const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups"
const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events"
// Call Events
@ -87,10 +87,12 @@ object EventType {
STATE_ROOM_JOIN_RULES,
STATE_ROOM_GUEST_ACCESS,
STATE_ROOM_POWER_LEVELS,
STATE_ROOM_ALIASES,
STATE_ROOM_TOMBSTONE,
STATE_HISTORY_VISIBILITY,
STATE_RELATED_GROUPS,
STATE_PINNED_EVENT
STATE_ROOM_CANONICAL_ALIAS,
STATE_ROOM_HISTORY_VISIBILITY,
STATE_ROOM_RELATED_GROUPS,
STATE_ROOM_PINNED_EVENT
)
fun isStateEvent(type: String): Boolean {

View file

@ -31,6 +31,13 @@ interface GroupService {
*/
fun getGroup(groupId: String): Group?
/**
* Get a groupSummary from a groupId
* @param groupId the groupId to look for.
* @return the groupSummary with groupId or null
*/
fun getGroupSummary(groupId: String): GroupSummary?
/**
* Get a live list of group summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of [GroupSummary]

View file

@ -52,6 +52,13 @@ interface RoomService {
*/
fun getRoom(roomId: String): Room?
/**
* Get a roomSummary from a roomId or a room alias
* @param roomIdOrAlias the roomId or the alias of a room to look for.
* @return a matching room summary or null
*/
fun getRoomSummary(roomIdOrAlias: String): RoomSummary?
/**
* Get a live list of room summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of [RoomSummary]

View file

@ -20,7 +20,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Class representing the EventType.STATE_CANONICAL_ALIAS state event content
* Class representing the EventType.STATE_ROOM_CANONICAL_ALIAS state event content
*/
@JsonClass(generateAdapter = true)
data class RoomCanonicalAliasContent(

View file

@ -145,13 +145,13 @@ class CreateRoomParams {
*/
fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) {
// Remove the existing value if any.
initialStates?.removeAll { it.getClearType() == EventType.STATE_HISTORY_VISIBILITY }
initialStates?.removeAll { it.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY }
if (historyVisibility != null) {
val contentMap = HashMap<String, RoomHistoryVisibility>()
contentMap["history_visibility"] = historyVisibility
val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY,
val historyVisibilityEvent = Event(type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
stateKey = "",
content = contentMap.toContent())

View file

@ -25,7 +25,7 @@ data class VideoInfo(
/**
* The mimetype of the video e.g. "video/mp4".
*/
@Json(name = "mimetype") val mimeType: String,
@Json(name = "mimetype") val mimeType: String?,
/**
* The width of the video in pixels.

View file

@ -98,7 +98,7 @@ interface RelationService {
/**
* Reply to an event in the timeline (must be in same room)
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
* The replyText can be a Spannable and contains special spans (UserMentionSpan) that will be translated
* The replyText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated
* by the sdk into pills.
* @param eventReplied the event referenced by the reply
* @param replyText the reply text

View file

@ -19,9 +19,9 @@ package im.vector.matrix.android.api.session.room.send
import im.vector.matrix.android.api.util.MatrixItem
/**
* Tag class for spans that should mention a user.
* Tag class for spans that should mention a matrix item.
* These Spans will be transformed into pills when detected in message to send
*/
interface UserMentionSpan {
interface MatrixItemSpan {
val matrixItem: MatrixItem
}

View file

@ -29,7 +29,7 @@ interface SendService {
/**
* Method to send a text message asynchronously.
* The text to send can be a Spannable and contains special spans (UserMentionSpan) that will be translated
* The text to send can be a Spannable and contains special spans (MatrixItemSpan) that will be translated
* by the sdk into pills.
* @param text the text message to send
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE

View file

@ -65,7 +65,7 @@ interface Timeline {
/**
* This is the main method to enrich the timeline with new data.
* It will call the onUpdated method from [Listener] when the data will be processed.
* It will call the onTimelineUpdated method from [Listener] when the data will be processed.
* It also ensures only one pagination by direction is launched at a time, so you can safely call this multiple time in a row.
*/
fun paginate(direction: Direction, count: Int)
@ -106,7 +106,12 @@ interface Timeline {
* Call when the timeline has been updated through pagination or sync.
* @param snapshot the most up to date snapshot
*/
fun onUpdated(snapshot: List<TimelineEvent>)
fun onTimelineUpdated(snapshot: List<TimelineEvent>)
/**
* Called whenever an error we can't recover from occurred
*/
fun onTimelineFailure(throwable: Throwable)
}
/**

View file

@ -104,7 +104,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
root.getClearContent().toModel<MessageStickerContent>()
} else {
annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel()
?: root.getClearContent().toModel()
}
}
@ -116,7 +116,7 @@ fun TimelineEvent.getLastMessageBody(): String? {
if (lastMessageContent != null) {
return lastMessageContent.newContent?.toModel<MessageContent>()?.body
?: lastMessageContent.body
?: lastMessageContent.body
}
return null

View file

@ -62,6 +62,9 @@ sealed class MatrixItem(
init {
if (BuildConfig.DEBUG) checkId()
}
// Best name is the id, and we keep the displayName of the room for the case we need the first letter
override fun getBestName() = id
}
data class GroupItem(override val id: String,
@ -71,9 +74,12 @@ sealed class MatrixItem(
init {
if (BuildConfig.DEBUG) checkId()
}
// Best name is the id, and we keep the displayName of the room for the case we need the first letter
override fun getBestName() = id
}
fun getBestName(): String {
open fun getBestName(): String {
return displayName?.takeIf { it.isNotBlank() } ?: id
}
@ -95,7 +101,7 @@ sealed class MatrixItem(
}
fun firstLetterOfDisplayName(): String {
return getBestName()
return (displayName?.takeIf { it.isNotBlank() } ?: id)
.let { dn ->
var startIndex = 0
val initial = dn[startIndex]
@ -138,4 +144,5 @@ sealed class MatrixItem(
fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl)
fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl)

View file

@ -149,17 +149,17 @@ internal class DefaultCryptoService @Inject constructor(
fun onStateEvent(roomId: String, event: Event) {
when {
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
}
}
fun onLiveEvent(roomId: String, event: Event) {
when {
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
}
}

View file

@ -49,7 +49,7 @@ object MXEncryptedAttachments {
* @param mimetype the mime type
* @return the encryption file info
*/
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult {
fun encryptAttachment(attachmentStream: InputStream, mimetype: String?): EncryptionResult {
val t0 = System.currentTimeMillis()
val secureRandom = SecureRandom()

View file

@ -19,12 +19,16 @@ package im.vector.matrix.android.internal.database
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.util.createBackgroundHandler
import io.realm.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
internal interface LiveEntityObserver {
fun start()
fun dispose()
fun cancelProcess()
fun isStarted(): Boolean
}
@ -35,6 +39,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r
val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND")
}
protected val observerScope = CoroutineScope(SupervisorJob())
protected abstract val query: Monarchy.Query<T>
private val isStarted = AtomicBoolean(false)
private val backgroundRealm = AtomicReference<Realm>()
@ -59,10 +64,15 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r
backgroundRealm.getAndSet(null).also {
it.close()
}
observerScope.coroutineContext.cancelChildren()
}
}
}
override fun cancelProcess() {
observerScope.coroutineContext.cancelChildren()
}
override fun isStarted(): Boolean {
return isStarted.get()
}

View file

@ -21,12 +21,7 @@ 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
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where

View file

@ -39,7 +39,6 @@ internal object MatrixModule {
computation = Dispatchers.Default,
main = Dispatchers.Main,
crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(),
sync = Executors.newSingleThreadExecutor().asCoroutineDispatcher(),
dmVerif = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
)
}

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.network
import android.content.Context
import androidx.annotation.WorkerThread
import com.novoda.merlin.Merlin
import com.novoda.merlin.MerlinsBeard
import im.vector.matrix.android.internal.di.MatrixScope
@ -28,8 +29,8 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@MatrixScope
internal class NetworkConnectivityChecker @Inject constructor(context: Context,
backgroundDetectionObserver: BackgroundDetectionObserver)
internal class NetworkConnectivityChecker @Inject constructor(private val context: Context,
private val backgroundDetectionObserver: BackgroundDetectionObserver)
: BackgroundDetectionObserver.Listener {
private val merlin = Merlin.Builder()
@ -37,19 +38,33 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context,
.withDisconnectableCallbacks()
.build(context)
private val listeners = Collections.synchronizedSet(LinkedHashSet<Listener>())
private val merlinsBeard = MerlinsBeard.Builder().build(context)
// True when internet is available
var hasInternetAccess = MerlinsBeard.Builder().build(context).isConnected
private set
private val listeners = Collections.synchronizedSet(LinkedHashSet<Listener>())
private var hasInternetAccess = merlinsBeard.isConnected
init {
backgroundDetectionObserver.register(this)
}
/**
* Returns true when internet is available
*/
@WorkerThread
fun hasInternetAccess(): Boolean {
// If we are in background we have unbound merlin, so we have to check
return if (backgroundDetectionObserver.isInBackground) {
merlinsBeard.hasInternetAccess()
} else {
hasInternetAccess
}
}
override fun onMoveToForeground() {
merlin.bind()
merlinsBeard.hasInternetAccess {
hasInternetAccess = it
}
merlin.registerDisconnectable {
if (hasInternetAccess) {
Timber.v("On Disconnect")
@ -76,14 +91,17 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context,
merlin.unbind()
}
// In background you won't get notification as merlin is unbound
suspend fun waitUntilConnected() {
if (hasInternetAccess) {
return
} else {
Timber.v("Waiting for network...")
suspendCoroutine<Unit> { continuation ->
register(object : Listener {
override fun onConnect() {
unregister(this)
Timber.v("Connected to network...")
continuation.resume(Unit)
}
})

View file

@ -83,15 +83,13 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
if (elementToDecrypt != null) {
Timber.v("## decrypt file")
MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) ?: throw IllegalStateException("Decryption error")
} else {
inputStream
MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
?: throw IllegalStateException("Decryption error")
}
writeToFile(inputStream, destFile)
destFile
}
.map { inputStream ->
writeToFile(inputStream, destFile)
destFile
}
} else {
Try.just(destFile)
}

View file

@ -45,6 +45,8 @@ import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.session.sync.SyncTaskSequencer
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import kotlinx.coroutines.Dispatchers
@ -76,24 +78,26 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val secureStorageService: Lazy<SecureStorageService>,
private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver,
private val syncTokenStore: SyncTokenStore,
private val syncTaskSequencer: SyncTaskSequencer,
private val sessionParamsStore: SessionParamsStore,
private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>)
: Session,
RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(),
GroupService by groupService.get(),
UserService by userService.get(),
CryptoService by cryptoService.get(),
SignOutService by signOutService.get(),
FilterService by filterService.get(),
PushRuleService by pushRuleService.get(),
PushersService by pushersService.get(),
FileService by fileService.get(),
InitialSyncProgressService by initialSyncProgressService.get(),
SecureStorageService by secureStorageService.get(),
HomeServerCapabilitiesService by homeServerCapabilitiesService.get() {
RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(),
GroupService by groupService.get(),
UserService by userService.get(),
CryptoService by cryptoService.get(),
SignOutService by signOutService.get(),
FilterService by filterService.get(),
PushRuleService by pushRuleService.get(),
PushersService by pushersService.get(),
FileService by fileService.get(),
InitialSyncProgressService by initialSyncProgressService.get(),
SecureStorageService by secureStorageService.get(),
HomeServerCapabilitiesService by homeServerCapabilitiesService.get() {
private var isOpen = false
@ -149,12 +153,17 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
cryptoService.get().close()
isOpen = false
EventBus.getDefault().unregister(this)
syncTaskSequencer.close()
}
override fun syncState(): LiveData<SyncState> {
return getSyncThread().liveState()
}
override fun hasAlreadySynced(): Boolean {
return syncTokenStore.getLastToken() != null
}
private fun getSyncThread(): SyncThread {
return syncThread ?: syncThreadProvider.get().also {
syncThread = it
@ -164,23 +173,14 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
override fun clearCache(callback: MatrixCallback<Unit>) {
stopSync()
stopAnyBackgroundSync()
cacheService.get().clearCache(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
startSync(true)
callback.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
startSync(true)
callback.onFailure(failure)
}
})
liveEntityObservers.forEach { it.cancelProcess() }
cacheService.get().clearCache(callback)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onGlobalError(globalError: GlobalError) {
if (globalError is GlobalError.InvalidToken
&& globalError.softLogout) {
&& globalError.softLogout) {
// Mark the token has invalid
GlobalScope.launch(Dispatchers.IO) {
sessionParamsStore.setTokenInvalid(myUserId)

View file

@ -46,6 +46,7 @@ import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import im.vector.matrix.android.internal.session.user.UserModule
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
@Component(dependencies = [MatrixComponent::class],
modules = [
@ -69,6 +70,8 @@ import im.vector.matrix.android.internal.task.TaskExecutor
@SessionScope
internal interface SessionComponent {
fun coroutineDispatchers(): MatrixCoroutineDispatchers
fun session(): Session
fun syncTask(): SyncTask

View file

@ -43,9 +43,9 @@ internal class FileUploader @Inject constructor(@Authenticated
suspend fun uploadFile(file: File,
filename: String?,
mimeType: String,
mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val uploadBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull())
return upload(uploadBody, filename, progressListener)
}

View file

@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.fetchCopyMap
import javax.inject.Inject
internal class DefaultGroupService @Inject constructor(private val monarchy: Monarchy) : GroupService {
@ -33,6 +34,13 @@ internal class DefaultGroupService @Inject constructor(private val monarchy: Mon
return null
}
override fun getGroupSummary(groupId: String): GroupSummary? {
return monarchy.fetchCopyMap(
{ realm -> GroupSummaryEntity.where(realm, groupId).findFirst() },
{ it, _ -> it.asDomain() }
)
}
override fun liveGroupSummaries(): LiveData<List<GroupSummary>> {
return monarchy.findAllMappedWithChanges(
{ realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) },

View file

@ -22,6 +22,7 @@ import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.database.model.GroupEntity
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
import im.vector.matrix.android.internal.database.query.where
@ -31,6 +32,7 @@ import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWor
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmResults
import kotlinx.coroutines.launch
import javax.inject.Inject
private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER"
@ -49,14 +51,19 @@ internal class GroupSummaryUpdater @Inject constructor(private val context: Cont
.mapNotNull { results[it] }
fetchGroupsData(modifiedGroupEntity
.filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE }
.map { it.groupId }
.toList())
.filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE }
.map { it.groupId }
.toList())
deleteGroups(modifiedGroupEntity
modifiedGroupEntity
.filter { it.membership == Membership.LEAVE }
.map { it.groupId }
.toList())
.toList()
.also {
observerScope.launch {
deleteGroups(it)
}
}
}
private fun fetchGroupsData(groupIds: List<String>) {
@ -77,12 +84,9 @@ internal class GroupSummaryUpdater @Inject constructor(private val context: Cont
/**
* Delete the GroupSummaryEntity of left groups
*/
private fun deleteGroups(groupIds: List<String>) {
monarchy
.writeAsync { realm ->
GroupSummaryEntity.where(realm, groupIds)
.findAll()
.deleteAllFromRealm()
}
private suspend fun deleteGroups(groupIds: List<String>) = awaitTransaction(monarchy.realmConfiguration) { realm ->
GroupSummaryEntity.where(realm, groupIds)
.findAll()
.deleteAllFromRealm()
}
}

View file

@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.findByAlias
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
@ -38,6 +39,7 @@ import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchCopyMap
import io.realm.Realm
import javax.inject.Inject
@ -69,6 +71,21 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
}
}
override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
return monarchy
.fetchCopyMap({
if (roomIdOrAlias.startsWith("!")) {
// It's a roomId
RoomSummaryEntity.where(it, roomId = roomIdOrAlias).findFirst()
} else {
// Assume it's a room alias
RoomSummaryEntity.findByAlias(it, roomIdOrAlias)
}
}, { entity, _ ->
roomSummaryMapper.map(entity)
})
}
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges(
{ realm ->

View file

@ -23,11 +23,10 @@ import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -39,8 +38,7 @@ import javax.inject.Inject
internal class EventRelationsAggregationUpdater @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
@UserId private val userId: String,
private val task: EventRelationsAggregationTask,
private val taskExecutor: TaskExecutor) :
private val task: EventRelationsAggregationTask) :
RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
override val query = Monarchy.Query<EventEntity> {
@ -69,6 +67,8 @@ internal class EventRelationsAggregationUpdater @Inject constructor(@SessionData
insertedDomains,
userId
)
task.configureWith(params).executeBy(taskExecutor)
observerScope.launch {
task.execute(params)
}
}
}

View file

@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
import im.vector.matrix.android.internal.session.room.notification.DefaultRoomPushRuleService
@ -35,6 +36,7 @@ internal interface RoomFactory {
fun create(roomId: String): Room
}
@SessionScope
internal class DefaultRoomFactory @Inject constructor(private val monarchy: Monarchy,
private val roomSummaryMapper: RoomSummaryMapper,
private val cryptoService: CryptoService,

View file

@ -28,6 +28,7 @@ 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.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.*
import im.vector.matrix.android.internal.database.query.isEventRead
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.prev
@ -38,7 +39,6 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
import io.realm.Realm
import io.realm.kotlin.createObject
import javax.inject.Inject
internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId: String,
@ -52,7 +52,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_HISTORY_VISIBILITY,
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER,
@ -69,9 +69,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
roomSummary: RoomSyncSummary? = null,
unreadNotifications: RoomSyncUnreadNotifications? = null,
updateMembers: Boolean = false) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
if (roomSummary != null) {
if (roomSummary.heroes.isNotEmpty()) {
roomSummaryEntity.heroes.clear()
@ -93,7 +91,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev()
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev()
val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0

View file

@ -52,7 +52,7 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro
apiCall = roomAPI.createRoom(params)
}
val roomId = createRoomResponse.roomId!!
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
// Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm ->
realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId)

View file

@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.VersioningState
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
@ -30,9 +31,9 @@ import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase
import io.realm.OrderedCollectionChangeSet
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import javax.inject.Inject
internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase
@ -51,21 +52,21 @@ internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase
}
.toList()
.also {
handleRoomCreateEvents(it)
observerScope.launch {
handleRoomCreateEvents(it)
}
}
}
private fun handleRoomCreateEvents(createEvents: List<Event>) = Realm.getInstance(realmConfiguration).use {
it.executeTransactionAsync { realm ->
for (event in createEvents) {
val createRoomContent = event.getClearContent().toModel<RoomCreateContent>()
val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue
private suspend fun handleRoomCreateEvents(createEvents: List<Event>) = awaitTransaction(realmConfiguration) { realm ->
for (event in createEvents) {
val createRoomContent = event.getClearContent().toModel<RoomCreateContent>()
val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue
val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst()
?: RoomSummaryEntity(predecessorRoomId)
predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED
realm.insertOrUpdate(predecessorRoomSummary)
}
val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst()
?: RoomSummaryEntity(predecessorRoomId)
predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED
realm.insertOrUpdate(predecessorRoomSummary)
}
}
}

View file

@ -62,7 +62,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
return@doWithRealm
}
val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev()
val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev()
name = ContentMapper.map(canonicalAlias?.content).toModel<RoomCanonicalAliasContent>()?.canonicalAlias
if (!name.isNullOrEmpty()) {
return@doWithRealm

View file

@ -23,11 +23,10 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -36,8 +35,7 @@ import javax.inject.Inject
* As it will actually delete the content, it should be called last in the list of listener.
*/
internal class EventsPruner @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
private val pruneEventTask: PruneEventTask,
private val taskExecutor: TaskExecutor) :
private val pruneEventTask: PruneEventTask) :
RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
override val query = Monarchy.Query<EventEntity> { EventEntity.types(it, listOf(EventType.REDACTION)) }
@ -50,7 +48,9 @@ internal class EventsPruner @Inject constructor(@SessionDatabase realmConfigurat
.mapNotNull { results[it]?.asDomain() }
.toList()
val params = PruneEventTask.Params(insertedDomains)
pruneEventTask.configureWith(params).executeBy(taskExecutor)
observerScope.launch {
val params = PruneEventTask.Params(insertedDomains)
pruneEventTask.execute(params)
}
}
}

View file

@ -105,10 +105,10 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
private fun computeAllowedKeys(type: String): List<String> {
// Add filtered content, allowed keys in content depends on the event type
return when (type) {
EventType.STATE_ROOM_MEMBER -> listOf("membership")
EventType.STATE_ROOM_CREATE -> listOf("creator")
EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule")
EventType.STATE_ROOM_POWER_LEVELS -> listOf("users",
EventType.STATE_ROOM_MEMBER -> listOf("membership")
EventType.STATE_ROOM_CREATE -> listOf("creator")
EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule")
EventType.STATE_ROOM_POWER_LEVELS -> listOf("users",
"users_default",
"events",
"events_default",
@ -117,10 +117,10 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
"kick",
"redact",
"invite")
EventType.STATE_ROOM_ALIASES -> listOf("aliases")
EventType.STATE_CANONICAL_ALIAS -> listOf("alias")
EventType.FEEDBACK -> listOf("type", "target_event_id")
else -> emptyList()
EventType.STATE_ROOM_ALIASES -> listOf("aliases")
EventType.STATE_ROOM_CANONICAL_ALIAS -> listOf("alias")
EventType.FEEDBACK -> listOf("type", "target_event_id")
else -> emptyList()
}
}
}

View file

@ -251,7 +251,7 @@ internal class LocalEchoEventFactory @Inject constructor(
type = MessageType.MSGTYPE_AUDIO,
body = attachment.name ?: "audio",
audioInfo = AudioInfo(
mimeType = attachment.mimeType.takeIf { it.isNotBlank() } ?: "audio/mpeg",
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
size = attachment.size
),
url = attachment.path
@ -264,7 +264,7 @@ internal class LocalEchoEventFactory @Inject constructor(
type = MessageType.MSGTYPE_FILE,
body = attachment.name ?: "file",
info = FileInfo(
mimeType = attachment.mimeType.takeIf { it.isNotBlank() }
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() }
?: "application/octet-stream",
size = attachment.size
),

View file

@ -16,10 +16,10 @@
package im.vector.matrix.android.internal.session.room.send.pills
import im.vector.matrix.android.api.session.room.send.UserMentionSpan
import im.vector.matrix.android.api.session.room.send.MatrixItemSpan
internal data class MentionLinkSpec(
val span: UserMentionSpan,
val span: MatrixItemSpan,
val start: Int,
val end: Int
)

View file

@ -16,15 +16,13 @@
package im.vector.matrix.android.internal.session.room.send.pills
import android.text.SpannableString
import im.vector.matrix.android.api.session.room.send.UserMentionSpan
import im.vector.matrix.android.api.session.room.send.MatrixItemSpan
import java.util.*
import javax.inject.Inject
/**
* Utility class to detect special span in CharSequence and turn them into
* formatted text to send them as a Matrix messages.
*
* For now only support UserMentionSpans (TODO rooms, room aliases, etc...)
*/
internal class TextPillsUtils @Inject constructor(
private val mentionLinkSpecComparator: MentionLinkSpecComparator
@ -49,7 +47,7 @@ internal class TextPillsUtils @Inject constructor(
private fun transformPills(text: CharSequence, template: String): String? {
val spannableString = SpannableString.valueOf(text)
val pills = spannableString
?.getSpans(0, text.length, UserMentionSpan::class.java)
?.getSpans(0, text.length, MatrixItemSpan::class.java)
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
?.toMutableList()
?.takeIf { it.isNotEmpty() }
@ -65,7 +63,7 @@ internal class TextPillsUtils @Inject constructor(
// append text before pill
append(text, currIndex, start)
// append the pill
append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.displayName))
append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.getBestName()))
currIndex = end
}
// append text after the last pill

View file

@ -504,7 +504,6 @@ internal class DefaultTimeline(
Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask
.configureWith(params) {
this.retryCount = Int.MAX_VALUE
this.constraints = TaskConstraints(connectedToNetwork = true)
this.callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
@ -524,6 +523,8 @@ internal class DefaultTimeline(
}
override fun onFailure(failure: Throwable) {
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
postSnapshot()
Timber.v("Failure fetching $limit items $direction from pagination request")
}
}
@ -637,7 +638,14 @@ internal class DefaultTimeline(
private fun fetchEvent(eventId: String) {
val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize)
cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor)
cancelableBag += contextOfEventTask.configureWith(params) {
callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onFailure(failure: Throwable) {
postFailure(failure)
}
}
}
.executeBy(taskExecutor)
}
private fun postSnapshot() {
@ -650,7 +658,7 @@ internal class DefaultTimeline(
val runnable = Runnable {
synchronized(listeners) {
listeners.forEach {
it.onUpdated(snapshot)
it.onTimelineUpdated(snapshot)
}
}
}
@ -658,6 +666,20 @@ internal class DefaultTimeline(
}
}
private fun postFailure(throwable: Throwable) {
if (isReady.get().not()) {
return
}
val runnable = Runnable {
synchronized(listeners) {
listeners.forEach {
it.onTimelineFailure(throwable)
}
}
}
mainHandler.post(runnable)
}
private fun clearAllValues() {
prevDisplayIndex = null
nextDisplayIndex = null

View file

@ -18,6 +18,10 @@ package im.vector.matrix.android.internal.session.room.timeline
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.helper.*
import im.vector.matrix.android.internal.database.helper.add
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvent
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
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.create
@ -112,7 +116,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
?: realm.createObject(roomId)
val nextToken: String?
val prevToken: String?
@ -125,7 +129,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
}
val shouldSkip = ChunkEntity.find(realm, roomId, nextToken = nextToken) != null
|| ChunkEntity.find(realm, roomId, prevToken = prevToken) != null
|| ChunkEntity.find(realm, roomId, prevToken = prevToken) != null
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
@ -139,7 +143,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
} else {
nextChunk?.apply { this.prevToken = prevToken }
}
?: ChunkEntity.create(realm, prevToken, nextToken)
?: ChunkEntity.create(realm, prevToken, nextToken)
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
Timber.v("Reach end of $roomId")

View file

@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.VersioningState
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
@ -30,9 +31,9 @@ import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase
import io.realm.OrderedCollectionChangeSet
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import javax.inject.Inject
internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDatabase
@ -51,24 +52,24 @@ internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDataba
}
.toList()
.also {
handleRoomTombstoneEvents(it)
observerScope.launch {
handleRoomTombstoneEvents(it)
}
}
}
private fun handleRoomTombstoneEvents(tombstoneEvents: List<Event>) = Realm.getInstance(realmConfiguration).use {
it.executeTransactionAsync { realm ->
for (event in tombstoneEvents) {
if (event.roomId == null) continue
val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>()
if (createRoomContent?.replacementRoom == null) continue
private suspend fun handleRoomTombstoneEvents(tombstoneEvents: List<Event>) = awaitTransaction(realmConfiguration) { realm ->
for (event in tombstoneEvents) {
if (event.roomId == null) continue
val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>()
if (createRoomContent?.replacementRoom == null) continue
val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst()
?: RoomSummaryEntity(event.roomId)
if (predecessorRoomSummary.versioningState == VersioningState.NONE) {
predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED
}
realm.insertOrUpdate(predecessorRoomSummary)
val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst()
?: RoomSummaryEntity(event.roomId)
if (predecessorRoomSummary.versioningState == VersioningState.NONE) {
predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED
}
realm.insertOrUpdate(predecessorRoomSummary)
}
}
}

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.session.sync
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.R
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.internal.database.model.GroupEntity
@ -25,11 +24,10 @@ import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressServi
import im.vector.matrix.android.internal.session.mapWithProgress
import im.vector.matrix.android.internal.session.sync.model.GroupsSyncResponse
import im.vector.matrix.android.internal.session.sync.model.InvitedGroupSync
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm
import javax.inject.Inject
internal class GroupSyncHandler @Inject constructor(private val monarchy: Monarchy) {
internal class GroupSyncHandler @Inject constructor() {
sealed class HandlingStrategy {
data class JOINED(val data: Map<String, Any>) : HandlingStrategy()
@ -37,12 +35,14 @@ internal class GroupSyncHandler @Inject constructor(private val monarchy: Monarc
data class LEFT(val data: Map<String, Any>) : HandlingStrategy()
}
suspend fun handle(roomsSyncResponse: GroupsSyncResponse, reporter: DefaultInitialSyncProgressService? = null) {
monarchy.awaitTransaction { realm ->
handleGroupSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), reporter)
handleGroupSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), reporter)
handleGroupSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), reporter)
}
fun handle(
realm: Realm,
roomsSyncResponse: GroupsSyncResponse,
reporter: DefaultInitialSyncProgressService? = null
) {
handleGroupSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), reporter)
handleGroupSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), reporter)
handleGroupSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), reporter)
}
// PRIVATE METHODS *****************************************************************************

View file

@ -16,9 +16,7 @@
package im.vector.matrix.android.internal.session.sync
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.R
import im.vector.matrix.android.api.pushrules.RuleScope
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
@ -34,31 +32,21 @@ import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoo
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
import im.vector.matrix.android.internal.session.mapWithProgress
import im.vector.matrix.android.internal.session.notification.DefaultPushRuleService
import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.session.sync.model.*
import im.vector.matrix.android.internal.session.user.UserEntityFactory
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm
import io.realm.kotlin.createObject
import timber.log.Timber
import javax.inject.Inject
internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarchy,
private val readReceiptHandler: ReadReceiptHandler,
internal class RoomSyncHandler @Inject constructor(private val readReceiptHandler: ReadReceiptHandler,
private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomTagHandler: RoomTagHandler,
private val roomFullyReadHandler: RoomFullyReadHandler,
private val cryptoService: DefaultCryptoService,
private val tokenStore: SyncTokenStore,
private val pushRuleService: DefaultPushRuleService,
private val processForPushTask: ProcessEventForPushTask,
private val taskExecutor: TaskExecutor) {
private val cryptoService: DefaultCryptoService) {
sealed class HandlingStrategy {
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
@ -66,28 +54,16 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy()
}
suspend fun handle(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService? = null) {
fun handle(
realm: Realm,
roomsSyncResponse: RoomsSyncResponse,
isInitialSync: Boolean,
reporter: DefaultInitialSyncProgressService? = null
) {
Timber.v("Execute transaction from $this")
monarchy.awaitTransaction { realm ->
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter)
handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter)
handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter)
}
// handle event for bing rule checks
checkPushRules(roomsSyncResponse)
}
private fun checkPushRules(roomsSyncResponse: RoomsSyncResponse) {
Timber.v("[PushRules] --> checkPushRules")
if (tokenStore.getLastToken() == null) {
Timber.v("[PushRules] <-- No push rule check on initial sync")
return
} // nothing on initial sync
val rules = pushRuleService.getPushRules(RuleScope.GLOBAL)
processForPushTask.configureWith(ProcessEventForPushTask.Params(roomsSyncResponse, rules))
.executeBy(taskExecutor)
Timber.v("[PushRules] <-- Push task scheduled")
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter)
handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter)
handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter)
}
// PRIVATE METHODS *****************************************************************************
@ -139,7 +115,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
?: Int.MIN_VALUE
?: Int.MIN_VALUE
val untimelinedStateIndex = minStateIndex + 1
roomSync.state.events.forEach { event ->
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)

View file

@ -16,20 +16,30 @@
package im.vector.matrix.android.internal.session.sync
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.R
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.pushrules.RuleScope
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
import im.vector.matrix.android.internal.session.reportSubtask
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.util.awaitTransaction
import timber.log.Timber
import javax.inject.Inject
import kotlin.system.measureTimeMillis
internal class SyncResponseHandler @Inject constructor(private val roomSyncHandler: RoomSyncHandler,
internal class SyncResponseHandler @Inject constructor(private val monarchy: Monarchy,
private val roomSyncHandler: RoomSyncHandler,
private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
private val groupSyncHandler: GroupSyncHandler,
private val cryptoSyncHandler: CryptoSyncHandler,
private val cryptoService: DefaultCryptoService,
private val tokenStore: SyncTokenStore,
private val processEventForPushTask: ProcessEventForPushTask,
private val pushRuleService: PushRuleService,
private val initialSyncProgressService: DefaultInitialSyncProgressService) {
suspend fun handleResponse(syncResponse: SyncResponse, fromToken: String?) {
@ -45,26 +55,27 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
}.also {
Timber.v("Finish handling start cryptoService in $it ms")
}
val measure = measureTimeMillis {
// Handle the to device events before the room ones
// to ensure to decrypt them properly
measureTimeMillis {
Timber.v("Handle toDevice")
reportSubtask(reporter, R.string.initial_sync_start_importing_account_crypto, 100, 0.1f) {
if (syncResponse.toDevice != null) {
cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter)
}
}
}.also {
Timber.v("Finish handling toDevice in $it ms")
}
// Handle the to device events before the room ones
// to ensure to decrypt them properly
measureTimeMillis {
Timber.v("Handle toDevice")
reportSubtask(reporter, R.string.initial_sync_start_importing_account_crypto, 100, 0.1f) {
if (syncResponse.toDevice != null) {
cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter)
}
}
}.also {
Timber.v("Finish handling toDevice in $it ms")
}
// Start one big transaction
monarchy.awaitTransaction { realm ->
measureTimeMillis {
Timber.v("Handle rooms")
reportSubtask(reporter, R.string.initial_sync_start_importing_account_rooms, 100, 0.7f) {
if (syncResponse.rooms != null) {
roomSyncHandler.handle(syncResponse.rooms, isInitialSync, reporter)
roomSyncHandler.handle(realm, syncResponse.rooms, isInitialSync, reporter)
}
}
}.also {
@ -75,7 +86,7 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
reportSubtask(reporter, R.string.initial_sync_start_importing_account_groups, 100, 0.1f) {
Timber.v("Handle groups")
if (syncResponse.groups != null) {
groupSyncHandler.handle(syncResponse.groups, reporter)
groupSyncHandler.handle(realm, syncResponse.groups, reporter)
}
}
}.also {
@ -85,15 +96,32 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
measureTimeMillis {
reportSubtask(reporter, R.string.initial_sync_start_importing_account_data, 100, 0.1f) {
Timber.v("Handle accountData")
userAccountDataSyncHandler.handle(syncResponse.accountData, syncResponse.rooms?.invite)
userAccountDataSyncHandler.handle(realm, syncResponse.accountData)
}
}.also {
Timber.v("Finish handling accountData in $it ms")
}
Timber.v("On sync completed")
cryptoSyncHandler.onSyncCompleted(syncResponse)
tokenStore.saveToken(realm, syncResponse.nextBatch)
}
Timber.v("Finish handling sync in $measure ms")
// Everything else we need to do outside the transaction
syncResponse.rooms?.also {
checkPushRules(it, isInitialSync)
userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite)
}
Timber.v("On sync completed")
cryptoSyncHandler.onSyncCompleted(syncResponse)
}
private suspend fun checkPushRules(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean) {
Timber.v("[PushRules] --> checkPushRules")
if (isInitialSync) {
Timber.v("[PushRules] <-- No push rule check on initial sync")
return
} // nothing on initial sync
val rules = pushRuleService.getPushRules(RuleScope.GLOBAL)
processEventForPushTask.execute(ProcessEventForPushTask.Params(roomsSyncResponse, rules))
Timber.v("[PushRules] <-- Push task scheduled")
}
}

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.session.sync
import im.vector.matrix.android.R
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
@ -26,6 +25,7 @@ import im.vector.matrix.android.internal.session.homeserver.GetHomeServerCapabil
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.session.user.UserStore
import im.vector.matrix.android.internal.task.Task
import timber.log.Timber
import javax.inject.Inject
internal interface SyncTask : Task<SyncTask.Params, Unit> {
@ -37,14 +37,19 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
@UserId private val userId: String,
private val filterRepository: FilterRepository,
private val syncResponseHandler: SyncResponseHandler,
private val sessionParamsStore: SessionParamsStore,
private val initialSyncProgressService: DefaultInitialSyncProgressService,
private val syncTokenStore: SyncTokenStore,
private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
private val userStore: UserStore
private val userStore: UserStore,
private val syncTaskSequencer: SyncTaskSequencer
) : SyncTask {
override suspend fun execute(params: SyncTask.Params) {
override suspend fun execute(params: SyncTask.Params) = syncTaskSequencer.post {
doSync(params)
}
private suspend fun doSync(params: SyncTask.Params) {
Timber.v("Sync task started on Thread: ${Thread.currentThread().name}")
// Maybe refresh the home server capabilities data we know
getHomeServerCapabilitiesTask.execute(Unit)
@ -69,9 +74,9 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
apiCall = syncAPI.sync(requestParams)
}
syncResponseHandler.handleResponse(syncResponse, token)
syncTokenStore.saveToken(syncResponse.nextBatch)
if (isInitialSync) {
initialSyncProgressService.endAll()
}
Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}")
}
}

View file

@ -0,0 +1,24 @@
/*
* 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.sync
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.ChannelCoroutineSequencer
import javax.inject.Inject
@SessionScope
internal class SyncTaskSequencer @Inject constructor() : ChannelCoroutineSequencer<Unit>()

View file

@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.sync
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.model.SyncEntity
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm
import javax.inject.Inject
@ -30,10 +29,8 @@ internal class SyncTokenStore @Inject constructor(private val monarchy: Monarchy
}
}
suspend fun saveToken(token: String?) {
monarchy.awaitTransaction {
val sync = SyncEntity(token)
it.insertOrUpdate(sync)
}
fun saveToken(realm: Realm, token: String?) {
val sync = SyncEntity(token)
realm.insertOrUpdate(sync)
}
}

View file

@ -17,44 +17,39 @@
package im.vector.matrix.android.internal.session.sync
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.pushrules.RuleScope
import im.vector.matrix.android.api.pushrules.RuleSetKey
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.query.getDirectRooms
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.pushers.SavePushRulesTask
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
import im.vector.matrix.android.internal.session.sync.model.accountdata.*
import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper
import im.vector.matrix.android.internal.session.user.accountdata.SaveBreadcrumbsTask
import im.vector.matrix.android.internal.session.user.accountdata.SaveIgnoredUsersTask
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm
import io.realm.RealmList
import timber.log.Timber
import javax.inject.Inject
internal class UserAccountDataSyncHandler @Inject constructor(private val monarchy: Monarchy,
@UserId private val userId: String,
private val directChatsHelper: DirectChatsHelper,
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val savePushRulesTask: SavePushRulesTask,
private val saveIgnoredUsersTask: SaveIgnoredUsersTask,
private val saveBreadcrumbsTask: SaveBreadcrumbsTask,
private val taskExecutor: TaskExecutor) {
private val updateUserAccountDataTask: UpdateUserAccountDataTask) {
suspend fun handle(accountData: UserAccountDataSync?, invites: Map<String, InvitedRoomSync>?) {
fun handle(realm: Realm, accountData: UserAccountDataSync?) {
accountData?.list?.forEach {
when (it) {
is UserAccountDataDirectMessages -> handleDirectChatRooms(it)
is UserAccountDataPushRules -> handlePushRules(it)
is UserAccountDataIgnoredUsers -> handleIgnoredUsers(it)
is UserAccountDataBreadcrumbs -> handleBreadcrumbs(it)
is UserAccountDataDirectMessages -> handleDirectChatRooms(realm, it)
is UserAccountDataPushRules -> handlePushRules(realm, it)
is UserAccountDataIgnoredUsers -> handleIgnoredUsers(realm, it)
is UserAccountDataBreadcrumbs -> handleBreadcrumbs(realm, it)
is UserAccountDataFallback -> Timber.d("Receive account data of unhandled type ${it.type}")
else -> error("Missing code here!")
}
@ -65,78 +60,133 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
// it.toString()
// MoshiProvider.providesMoshi()
// }
monarchy.doWithRealm { realm ->
synchronizeWithServerIfNeeded(realm, invites)
}
}
private suspend fun handlePushRules(userAccountDataPushRules: UserAccountDataPushRules) {
savePushRulesTask.execute(SavePushRulesTask.Params(userAccountDataPushRules.content))
}
private suspend fun handleDirectChatRooms(directMessages: UserAccountDataDirectMessages) {
monarchy.awaitTransaction { realm ->
val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
oldDirectRooms.forEach {
it.isDirect = false
it.directUserId = null
}
directMessages.content.forEach {
val userId = it.key
it.value.forEach { roomId ->
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
if (roomSummaryEntity != null) {
roomSummaryEntity.isDirect = true
roomSummaryEntity.directUserId = userId
realm.insertOrUpdate(roomSummaryEntity)
}
}
}
}
}
// If we get some direct chat invites, we synchronize the user account data including those.
private fun synchronizeWithServerIfNeeded(realm: Realm, invites: Map<String, InvitedRoomSync>?) {
suspend fun synchronizeWithServerIfNeeded(invites: Map<String, InvitedRoomSync>) {
if (invites.isNullOrEmpty()) return
val directChats = directChatsHelper.getLocalUserAccount()
var hasUpdate = false
invites.forEach { (roomId, _) ->
val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(userId)
val inviterId = myUserStateEvent?.sender
val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() }
val isDirect = myUserRoomMember?.isDirect
if (inviterId != null && inviterId != userId && isDirect == true) {
directChats
.getOrPut(inviterId, { arrayListOf() })
.apply {
if (contains(roomId)) {
Timber.v("Direct chats already include room $roomId with user $inviterId")
} else {
add(roomId)
hasUpdate = true
monarchy.doWithRealm { realm ->
invites.forEach { (roomId, _) ->
val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(userId)
val inviterId = myUserStateEvent?.sender
val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() }
val isDirect = myUserRoomMember?.isDirect
if (inviterId != null && inviterId != userId && isDirect == true) {
directChats
.getOrPut(inviterId, { arrayListOf() })
.apply {
if (contains(roomId)) {
Timber.v("Direct chats already include room $roomId with user $inviterId")
} else {
add(roomId)
hasUpdate = true
}
}
}
}
}
}
if (hasUpdate) {
val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams(
directMessages = directChats
)
updateUserAccountDataTask.configureWith(updateUserAccountParams).executeBy(taskExecutor)
updateUserAccountDataTask.execute(updateUserAccountParams)
}
}
private fun handleIgnoredUsers(userAccountDataIgnoredUsers: UserAccountDataIgnoredUsers) {
saveIgnoredUsersTask
.configureWith(SaveIgnoredUsersTask.Params(userAccountDataIgnoredUsers.content.ignoredUsers.keys.toList()))
.executeBy(taskExecutor)
private fun handlePushRules(realm: Realm, userAccountDataPushRules: UserAccountDataPushRules) {
val pushRules = userAccountDataPushRules.content
realm.where(PushRulesEntity::class.java)
.findAll()
.deleteAllFromRealm()
// Save only global rules for the moment
val globalRules = pushRules.global
val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT }
globalRules.content?.forEach { rule ->
content.pushRules.add(PushRulesMapper.map(rule))
}
realm.insertOrUpdate(content)
val override = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.OVERRIDE }
globalRules.override?.forEach { rule ->
PushRulesMapper.map(rule).also {
override.pushRules.add(it)
}
}
realm.insertOrUpdate(override)
val rooms = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.ROOM }
globalRules.room?.forEach { rule ->
rooms.pushRules.add(PushRulesMapper.map(rule))
}
realm.insertOrUpdate(rooms)
val senders = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.SENDER }
globalRules.sender?.forEach { rule ->
senders.pushRules.add(PushRulesMapper.map(rule))
}
realm.insertOrUpdate(senders)
val underrides = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.UNDERRIDE }
globalRules.underride?.forEach { rule ->
underrides.pushRules.add(PushRulesMapper.map(rule))
}
realm.insertOrUpdate(underrides)
}
private fun handleDirectChatRooms(realm: Realm, directMessages: UserAccountDataDirectMessages) {
val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
oldDirectRooms.forEach {
it.isDirect = false
it.directUserId = null
}
directMessages.content.forEach {
val userId = it.key
it.value.forEach { roomId ->
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
if (roomSummaryEntity != null) {
roomSummaryEntity.isDirect = true
roomSummaryEntity.directUserId = userId
realm.insertOrUpdate(roomSummaryEntity)
}
}
}
}
private fun handleIgnoredUsers(realm: Realm, userAccountDataIgnoredUsers: UserAccountDataIgnoredUsers) {
val userIds = userAccountDataIgnoredUsers.content.ignoredUsers.keys
realm.where(IgnoredUserEntity::class.java)
.findAll()
.deleteAllFromRealm()
// And save the new received list
userIds.forEach { realm.createObject(IgnoredUserEntity::class.java).apply { userId = it } }
// TODO If not initial sync, we should execute a init sync
}
private fun handleBreadcrumbs(userAccountDataBreadcrumbs: UserAccountDataBreadcrumbs) {
saveBreadcrumbsTask
.configureWith(SaveBreadcrumbsTask.Params(userAccountDataBreadcrumbs.content.recentRoomIds))
.executeBy(taskExecutor)
private fun handleBreadcrumbs(realm: Realm, userAccountDataBreadcrumbs: UserAccountDataBreadcrumbs) {
val recentRoomIds = userAccountDataBreadcrumbs.content.recentRoomIds
val entity = BreadcrumbsEntity.getOrCreate(realm)
// And save the new received list
entity.recentRoomIds = RealmList<String>().apply { addAll(recentRoomIds) }
// Update the room summaries
// Reset all the indexes...
RoomSummaryEntity.where(realm)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
.findAll()
.forEach {
it.breadcrumbsIndex = RoomSummaryEntity.NOT_IN_BREADCRUMBS
}
// ...and apply new indexes
recentRoomIds.forEachIndexed { index, roomId ->
RoomSummaryEntity.where(realm, roomId)
.findFirst()
?.breadcrumbsIndex = index
}
}
}

View file

@ -18,21 +18,18 @@ package im.vector.matrix.android.internal.session.sync.job
import android.app.Service
import android.content.Intent
import android.os.IBinder
import com.squareup.moshi.JsonEncodingException
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.failure.isTokenError
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.sync.SyncTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.*
import timber.log.Timber
import java.net.SocketTimeoutException
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.atomic.AtomicBoolean
/**
* Can execute periodic sync task.
@ -40,33 +37,46 @@ import java.util.TimerTask
* in order to be able to perform a sync even if the app is not running.
* The <receiver> and <service> must be declared in the Manifest or the app using the SDK
*/
open class SyncService : Service() {
abstract class SyncService : Service() {
private var userId: String? = null
private var mIsSelfDestroyed: Boolean = false
private var cancelableTask: Cancelable? = null
private var isInitialSync: Boolean = false
private lateinit var session: Session
private lateinit var syncTask: SyncTask
private lateinit var networkConnectivityChecker: NetworkConnectivityChecker
private lateinit var taskExecutor: TaskExecutor
private lateinit var coroutineDispatchers: MatrixCoroutineDispatchers
private lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
var timer = Timer()
private val isRunning = AtomicBoolean(false)
private val serviceScope = CoroutineScope(SupervisorJob())
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.i("onStartCommand $intent")
intent?.let {
val userId = it.getStringExtra(EXTRA_USER_ID)
val sessionComponent = Matrix.getInstance(applicationContext).sessionManager.getSessionComponent(userId)
val matrix = Matrix.getInstance(applicationContext)
val safeUserId = it.getStringExtra(EXTRA_USER_ID) ?: return@let
val sessionComponent = matrix.sessionManager.getSessionComponent(safeUserId)
?: return@let
session = sessionComponent.session()
userId = safeUserId
syncTask = sessionComponent.syncTask()
isInitialSync = !session.hasAlreadySynced()
networkConnectivityChecker = sessionComponent.networkConnectivityChecker()
taskExecutor = sessionComponent.taskExecutor()
if (cancelableTask == null) {
timer.cancel()
timer = Timer()
doSync(true)
coroutineDispatchers = sessionComponent.coroutineDispatchers()
backgroundDetectionObserver = matrix.backgroundDetectionObserver
onStart(isInitialSync)
if (isRunning.get()) {
Timber.i("Received a start while was already syncing... ignore")
} else {
// Already syncing ignore
Timber.i("Received a start while was already syncking... ignore")
isRunning.set(true)
serviceScope.launch(coroutineDispatchers.io) {
doSync()
}
}
}
// No intent just start the service, an alarm will should call with intent
@ -75,98 +85,61 @@ open class SyncService : Service() {
override fun onDestroy() {
Timber.i("## onDestroy() : $this")
if (!mIsSelfDestroyed) {
Timber.w("## Destroy by the system : $this")
}
cancelableTask?.cancel()
serviceScope.coroutineContext.cancelChildren()
isRunning.set(false)
super.onDestroy()
}
fun stopMe() {
timer.cancel()
timer = Timer()
cancelableTask?.cancel()
private fun stopMe() {
mIsSelfDestroyed = true
stopSelf()
}
fun doSync(once: Boolean = false) {
if (!networkConnectivityChecker.hasInternetAccess) {
Timber.v("No internet access. Waiting...")
// TODO Retry in ?
timer.schedule(object : TimerTask() {
override fun run() {
doSync()
}
}, NO_NETWORK_DELAY)
} else {
Timber.v("Execute sync request with timeout 0")
val params = SyncTask.Params(TIME_OUT)
cancelableTask = syncTask
.configureWith(params) {
callbackThread = TaskThread.SYNC
executionThread = TaskThread.SYNC
callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
cancelableTask = null
if (!once) {
timer.schedule(object : TimerTask() {
override fun run() {
doSync()
}
}, NEXT_BATCH_DELAY)
} else {
// stop
stopMe()
}
}
override fun onFailure(failure: Throwable) {
Timber.e(failure)
cancelableTask = null
if (failure is Failure.NetworkConnection
&& failure.cause is SocketTimeoutException) {
// Timeout are not critical
timer.schedule(object : TimerTask() {
override fun run() {
doSync()
}
}, 5_000L)
}
if (failure !is Failure.NetworkConnection
|| failure.cause is JsonEncodingException) {
// Wait 10s before retrying
timer.schedule(object : TimerTask() {
override fun run() {
doSync()
}
}, 5_000L)
}
if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
// No token or invalid token, stop the thread
stopSelf()
}
}
}
}
.executeBy(taskExecutor)
private suspend fun doSync() {
if (!networkConnectivityChecker.hasInternetAccess()) {
Timber.v("No network reschedule to avoid wasting resources")
userId?.also {
onRescheduleAsked(it, isInitialSync, delay = 10_000L)
}
stopMe()
return
}
Timber.v("Execute sync request with timeout 0")
val params = SyncTask.Params(TIME_OUT)
try {
syncTask.execute(params)
// Start sync if we were doing an initial sync and the syncThread is not launched yet
if (isInitialSync && session.syncState().value == SyncState.Idle) {
val isForeground = !backgroundDetectionObserver.isInBackground
session.startSync(isForeground)
}
stopMe()
} catch (throwable: Throwable) {
Timber.e(throwable)
if (throwable.isTokenError()) {
stopMe()
} else {
Timber.v("Retry to sync in 5s")
delay(DELAY_FAILURE)
doSync()
}
}
}
abstract fun onStart(isInitialSync: Boolean)
abstract fun onRescheduleAsked(userId: String, isInitialSync: Boolean, delay: Long)
override fun onBind(intent: Intent?): IBinder? {
return null
}
companion object {
const val EXTRA_USER_ID = "EXTRA_USER_ID"
const val TIME_OUT = 0L
const val NEXT_BATCH_DELAY = 60_000L
const val NO_NETWORK_DELAY = 5_000L
private const val TIME_OUT = 0L
private const val DELAY_FAILURE = 5_000L
}
}

View file

@ -19,20 +19,15 @@ package im.vector.matrix.android.internal.session.sync.job
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.squareup.moshi.JsonEncodingException
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.failure.isTokenError
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.sync.SyncTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import kotlinx.coroutines.*
import timber.log.Timber
import java.net.SocketTimeoutException
import java.util.concurrent.CountDownLatch
import javax.inject.Inject
private const val RETRY_WAIT_TIME_MS = 10_000L
@ -40,14 +35,13 @@ private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L
internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val networkConnectivityChecker: NetworkConnectivityChecker,
private val backgroundDetectionObserver: BackgroundDetectionObserver,
private val taskExecutor: TaskExecutor
) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
private val backgroundDetectionObserver: BackgroundDetectionObserver)
: Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
private var state: SyncState = SyncState.Idle
private var liveState = MutableLiveData<SyncState>()
private val lock = Object()
private var cancelableTask: Cancelable? = null
private val syncScope = CoroutineScope(SupervisorJob())
private var isStarted = false
private var isTokenValid = true
@ -75,14 +69,14 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
if (isStarted) {
Timber.v("Pause sync...")
isStarted = false
cancelableTask?.cancel()
syncScope.coroutineContext.cancelChildren()
}
}
fun kill() = synchronized(lock) {
Timber.v("Kill sync...")
updateStateTo(SyncState.Killing)
cancelableTask?.cancel()
syncScope.coroutineContext.cancelChildren()
lock.notify()
}
@ -102,11 +96,9 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
isStarted = true
networkConnectivityChecker.register(this)
backgroundDetectionObserver.register(this)
while (state != SyncState.Killing) {
Timber.v("Entering loop, state: $state")
if (!networkConnectivityChecker.hasInternetAccess) {
if (!networkConnectivityChecker.hasInternetAccess()) {
Timber.v("No network. Waiting...")
updateStateTo(SyncState.NoNetwork)
synchronized(lock) { lock.wait() }
@ -125,58 +117,16 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
if (state !is SyncState.Running) {
updateStateTo(SyncState.Running(afterPause = true))
}
// No timeout after a pause
val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
Timber.v("Execute sync request with timeout $timeout")
val latch = CountDownLatch(1)
val params = SyncTask.Params(timeout)
cancelableTask = syncTask.configureWith(params) {
this.callbackThread = TaskThread.SYNC
this.executionThread = TaskThread.SYNC
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.v("onSuccess")
latch.countDown()
}
override fun onFailure(failure: Throwable) {
if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) {
// Timeout are not critical
Timber.v("Timeout")
} else if (failure is Failure.Cancelled) {
Timber.v("Cancelled")
} else if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
// No token or invalid token
Timber.w(failure)
isTokenValid = false
isStarted = false
} else {
Timber.e(failure)
if (failure !is Failure.NetworkConnection || failure.cause is JsonEncodingException) {
// Wait 10s before retrying
Timber.v("Wait 10s")
sleep(RETRY_WAIT_TIME_MS)
}
}
latch.countDown()
}
}
val sync = syncScope.launch {
doSync(params)
}
.executeBy(taskExecutor)
latch.await()
state.let {
if (it is SyncState.Running && it.afterPause) {
updateStateTo(SyncState.Running(afterPause = false))
}
runBlocking {
sync.join()
}
Timber.v("...Continue")
}
}
@ -186,6 +136,37 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
networkConnectivityChecker.unregister(this)
}
private suspend fun doSync(params: SyncTask.Params) {
try {
syncTask.execute(params)
} catch (failure: Throwable) {
if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) {
// Timeout are not critical
Timber.v("Timeout")
} else if (failure is Failure.Cancelled) {
Timber.v("Cancelled")
} else if (failure.isTokenError()) {
// No token or invalid token, stop the thread
Timber.w(failure)
isStarted = false
isTokenValid = false
} else {
Timber.e(failure)
if (failure !is Failure.NetworkConnection || failure.cause is JsonEncodingException) {
// Wait 10s before retrying
Timber.v("Wait 10s")
delay(RETRY_WAIT_TIME_MS)
}
}
} finally {
state.let {
if (it is SyncState.Running && it.afterPause) {
updateStateTo(SyncState.Running(afterPause = false))
}
}
}
}
private fun updateStateTo(newState: SyncState) {
Timber.v("Update state from $state to $newState")
state = newState

View file

@ -18,14 +18,14 @@ package im.vector.matrix.android.internal.session.sync.job
import android.content.Context
import androidx.work.*
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.failure.isTokenError
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.sync.SyncTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.worker.WorkManagerUtil
import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.getSessionComponent
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -45,46 +45,58 @@ internal class SyncWorker(context: Context,
@Inject lateinit var syncTask: SyncTask
@Inject lateinit var taskExecutor: TaskExecutor
@Inject lateinit var coroutineDispatchers: MatrixCoroutineDispatchers
@Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker
override suspend fun doWork(): Result {
Timber.i("Sync work starting")
val params = WorkerParamsFactory.fromData<Params>(inputData) ?: return Result.success()
val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
sessionComponent.inject(this)
runCatching {
withContext(coroutineDispatchers.sync) {
val taskParams = SyncTask.Params(0)
syncTask.execute(taskParams)
}
}
return Result.success()
return runCatching {
doSync(params.timeout)
}.fold(
{ Result.success() },
{ failure ->
if (failure.isTokenError() || !params.automaticallyRetry) {
Result.failure()
} else {
Result.retry()
}
}
)
}
private suspend fun doSync(timeout: Long) {
val taskParams = SyncTask.Params(timeout)
syncTask.execute(taskParams)
}
companion object {
const val BG_SYNC_WORK_NAME = "BG_SYNCP"
fun requireBackgroundSync(context: Context, userId: String, serverTimeout: Long = 0) {
val data = WorkerParamsFactory.toData(Params(userId, serverTimeout, false))
val workRequest = matrixOneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(data)
.setConstraints(WorkManagerUtil.workConstraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS)
.setInputData(data)
.build()
WorkManager.getInstance(context).enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.REPLACE, workRequest)
WorkManager.getInstance(context).enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest)
}
fun automaticallyBackgroundSync(context: Context, userId: String, serverTimeout: Long = 0, delay: Long = 30_000) {
val data = WorkerParamsFactory.toData(Params(userId, serverTimeout, true))
val workRequest = matrixOneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(data)
.setConstraints(WorkManagerUtil.workConstraints)
.setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, delay, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance(context).enqueueUniqueWork("BG_SYNCP", ExistingWorkPolicy.REPLACE, workRequest)
WorkManager.getInstance(context).enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest)
}
fun stopAnyBackgroundSync(context: Context) {
WorkManager.getInstance(context).cancelUniqueWork("BG_SYNCP")
WorkManager.getInstance(context).cancelUniqueWork(BG_SYNC_WORK_NAME)
}
}
}

View file

@ -0,0 +1,87 @@
/*
* 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.task
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.util.concurrent.Executors
/**
* This class intends to be used for ensure suspendable methods are played sequentially all the way long.
*/
internal interface CoroutineSequencer<T> {
/**
* @param block the suspendable block to execute
* @return the result of the block
*/
suspend fun post(block: suspend () -> T): T
/**
* Cancel all and close, so you won't be able to post anything else after
*/
fun close()
}
internal open class ChannelCoroutineSequencer<T> : CoroutineSequencer<T> {
private data class Message<T>(
val block: suspend () -> T,
val deferred: CompletableDeferred<T>
)
private var messageChannel: Channel<Message<T>> = Channel()
private val coroutineScope = CoroutineScope(SupervisorJob())
// This will ensure
private val singleDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
init {
launchCoroutine()
}
private fun launchCoroutine() {
coroutineScope.launch(singleDispatcher) {
for (message in messageChannel) {
try {
val result = message.block()
message.deferred.complete(result)
} catch (exception: Throwable) {
message.deferred.completeExceptionally(exception)
}
}
}
}
override fun close() {
coroutineScope.coroutineContext.cancelChildren()
messageChannel.close()
}
override suspend fun post(block: suspend () -> T): T {
val deferred = CompletableDeferred<T>()
val message = Message(block, deferred)
messageChannel.send(message)
return try {
deferred.await()
} catch (cancellation: CancellationException) {
// In case of cancellation, we stop the current coroutine context
// and relaunch one to consume next messages
coroutineScope.coroutineContext.cancelChildren()
launchCoroutine()
throw cancellation
}
}
}

View file

@ -85,7 +85,6 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers
TaskThread.IO -> coroutineDispatchers.io
TaskThread.CALLER -> EmptyCoroutineContext
TaskThread.CRYPTO -> coroutineDispatchers.crypto
TaskThread.SYNC -> coroutineDispatchers.sync
TaskThread.DM_VERIF -> coroutineDispatchers.dmVerif
}
}

View file

@ -22,6 +22,5 @@ internal enum class TaskThread {
IO,
CALLER,
CRYPTO,
SYNC,
DM_VERIF
}

View file

@ -29,7 +29,7 @@ import javax.inject.Inject
@MatrixScope
internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObserver {
var isIsBackground: Boolean = false
var isInBackground: Boolean = false
private set
private
@ -46,14 +46,14 @@ internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObse
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onMoveToForeground() {
Timber.v("App returning to foreground…")
isIsBackground = false
isInBackground = false
listeners.forEach { it.onMoveToForeground() }
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onMoveToBackground() {
Timber.v("App going to background…")
isIsBackground = true
isInBackground = true
listeners.forEach { it.onMoveToBackground() }
}

View file

@ -23,6 +23,5 @@ internal data class MatrixCoroutineDispatchers(
val computation: CoroutineDispatcher,
val main: CoroutineDispatcher,
val crypto: CoroutineDispatcher,
val sync: CoroutineDispatcher,
val dmVerif: CoroutineDispatcher
)

View file

@ -82,4 +82,86 @@
<string name="room_displayname_empty_room">Prázdná místnost</string>
<string name="notice_room_update">%s upravil/a tuto místnost.</string>
<string name="notice_event_redacted_with_reason">Zpráva byla smazána [důvod: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Zpráva smazána [smazal/a %1$s] [důvod: %2$s]</string>
<string name="notice_room_third_party_revoked_invite">"%1$s obnovil/a pozvánku do místnosti pro %2$s"</string>
<string name="verification_emoji_cat">Kočka</string>
<string name="verification_emoji_lion">Lev</string>
<string name="verification_emoji_horse">Kůň</string>
<string name="verification_emoji_unicorn">Jednorožec</string>
<string name="verification_emoji_pig">Prase</string>
<string name="verification_emoji_elephant">Slon</string>
<string name="verification_emoji_rabbit">Králík</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Kohout</string>
<string name="verification_emoji_penguin">Tučnák</string>
<string name="verification_emoji_turtle">Želva</string>
<string name="verification_emoji_fish">Ryba</string>
<string name="verification_emoji_octopus">Chobotnice</string>
<string name="verification_emoji_butterfly">Motýl</string>
<string name="verification_emoji_flower">Květina</string>
<string name="verification_emoji_tree">Strom</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Houba</string>
<string name="verification_emoji_globe">Glóbus</string>
<string name="verification_emoji_moon">Měsíc</string>
<string name="verification_emoji_cloud">Mrak</string>
<string name="verification_emoji_fire">Oheň</string>
<string name="verification_emoji_banana">Banán</string>
<string name="verification_emoji_apple">Jablko</string>
<string name="verification_emoji_strawberry">Jahoda</string>
<string name="verification_emoji_corn">Kukuřice</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Dort</string>
<string name="verification_emoji_heart">Srdce</string>
<string name="verification_emoji_smiley">Smajlík</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Klobouk</string>
<string name="verification_emoji_glasses">Brýle</string>
<string name="verification_emoji_santa">Santa</string>
<string name="verification_emoji_thumbsup">Zvednutý palec</string>
<string name="verification_emoji_umbrella">Deštník</string>
<string name="verification_emoji_hourglass">Přesípací hodiny</string>
<string name="verification_emoji_clock">Hodiny</string>
<string name="verification_emoji_gift">Dárek</string>
<string name="verification_emoji_lightbulb">Žárovka</string>
<string name="verification_emoji_book">Knížka</string>
<string name="verification_emoji_pencil">Tužka</string>
<string name="verification_emoji_paperclip">Sponka</string>
<string name="verification_emoji_scissors">Nůžky</string>
<string name="verification_emoji_lock">Zámek</string>
<string name="verification_emoji_key">Klíč</string>
<string name="verification_emoji_hammer">Kladivo</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Vlajka</string>
<string name="verification_emoji_train">Vlak</string>
<string name="verification_emoji_bicycle">Kolo</string>
<string name="verification_emoji_airplane">Letadlo</string>
<string name="verification_emoji_rocket">Raketa</string>
<string name="verification_emoji_trophy">Pohár</string>
<string name="verification_emoji_ball">Míč</string>
<string name="verification_emoji_guitar">Kytara</string>
<string name="verification_emoji_trumpet">Trumpeta</string>
<string name="verification_emoji_bell">Zvon</string>
<string name="verification_emoji_anchor">Kotva</string>
<string name="verification_emoji_headphone">Sluchátka</string>
<string name="verification_emoji_folder">Složka</string>
<string name="initial_sync_start_importing_account">Úvodní synchronizace:
\nStahuji účet…</string>
<string name="initial_sync_start_importing_account_crypto">Uvodní synchronizace:
\nStahuji klíče</string>
<string name="initial_sync_start_importing_account_rooms">Uvodní synchnizace:
\nStahuji místnost</string>
<string name="initial_sync_start_importing_account_joined_rooms">Uvodní synchronizace:
\nStahuji moje místnosti</string>
<string name="initial_sync_start_importing_account_left_rooms">Uvodní synchonizace:
\nStahuji místnosti, které jsem opustil/a</string>
<string name="initial_sync_start_importing_account_groups">Úvodní sychronizace:
\nImportuji komunity</string>
<string name="initial_sync_start_importing_account_data">Úvodní synchronizace:
\nImportuji data účtu</string>
<string name="event_status_sending_message">Posílám zprávu…</string>
</resources>

View file

@ -49,7 +49,7 @@
<string name="notice_room_third_party_invite">%1$s님이 %2$s님에게 방 초대를 보냈습니다</string>
<string name="notice_room_third_party_registered_invite">%1$s님이 %2$s의 초대를 수락했습니다</string>
<string name="notice_crypto_unable_to_decrypt">** 암호를 해독할 수 없음: %s **</string>
<string name="notice_crypto_unable_to_decrypt">** 암호를 복호화할 수 없음: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.</string>
<string name="message_reply_to_prefix">관련 대화</string>

View file

@ -88,70 +88,70 @@
<string name="notice_event_redacted_by">Správa odstránená používateľom %1$s</string>
<string name="notice_event_redacted_with_reason">Správa odstránená [dôvod: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Správa odstránená používateľom %1$s [dôvod: %2$s]</string>
<string name="verification_emoji_dog">Pes</string>
<string name="verification_emoji_cat">Mačka</string>
<string name="verification_emoji_lion">Lev</string>
<string name="verification_emoji_dog">Hlava psa</string>
<string name="verification_emoji_cat">Hlava mačky</string>
<string name="verification_emoji_lion">Hlava leva</string>
<string name="verification_emoji_horse">Kôň</string>
<string name="verification_emoji_unicorn">Jednorožec</string>
<string name="verification_emoji_pig">Prasa</string>
<string name="verification_emoji_unicorn">Hlava jednorožca</string>
<string name="verification_emoji_pig">Hlava prasaťa</string>
<string name="verification_emoji_elephant">Slon</string>
<string name="verification_emoji_rabbit">Zajac</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rabbit">Hlava zajaca</string>
<string name="verification_emoji_panda">Hlava pandy</string>
<string name="verification_emoji_rooster">Kohút</string>
<string name="verification_emoji_penguin">Tučniak</string>
<string name="verification_emoji_turtle">Korytnačka</string>
<string name="verification_emoji_fish">Ryba</string>
<string name="verification_emoji_octopus">Chobotnica</string>
<string name="verification_emoji_butterfly">Motýľ</string>
<string name="verification_emoji_flower">Kvetina</string>
<string name="verification_emoji_tree">Strom</string>
<string name="verification_emoji_flower">Tulipán</string>
<string name="verification_emoji_tree">Listnatý strom</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Hríb</string>
<string name="verification_emoji_mushroom">Huba</string>
<string name="verification_emoji_globe">Zemeguľa</string>
<string name="verification_emoji_moon">Mesiac</string>
<string name="verification_emoji_moon">Polmesiac</string>
<string name="verification_emoji_cloud">Oblak</string>
<string name="verification_emoji_fire">Oheň</string>
<string name="verification_emoji_banana">Banán</string>
<string name="verification_emoji_apple">Jablko</string>
<string name="verification_emoji_apple">Červené jablko</string>
<string name="verification_emoji_strawberry">Jahoda</string>
<string name="verification_emoji_corn">Kukurica</string>
<string name="verification_emoji_corn">Kukuričný klas</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Koláč</string>
<string name="verification_emoji_heart">Srdce</string>
<string name="verification_emoji_smiley">Úsmev</string>
<string name="verification_emoji_cake">Narodeninová torta</string>
<string name="verification_emoji_heart">Červené</string>
<string name="verification_emoji_smiley">Škeriaca sa tvár</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Klobúk</string>
<string name="verification_emoji_hat">Cylinder</string>
<string name="verification_emoji_glasses">Okuliare</string>
<string name="verification_emoji_wrench">Skrutkovač</string>
<string name="verification_emoji_santa">Mikuláš</string>
<string name="verification_emoji_wrench">Francúzsky kľúč</string>
<string name="verification_emoji_santa">Santa Claus</string>
<string name="verification_emoji_thumbsup">Palec nahor</string>
<string name="verification_emoji_umbrella">Dáždnik</string>
<string name="verification_emoji_hourglass">Presýpacie hodiny</string>
<string name="verification_emoji_clock">Hodiny</string>
<string name="verification_emoji_gift">Darček</string>
<string name="verification_emoji_clock">Budík</string>
<string name="verification_emoji_gift">Zabalený darček</string>
<string name="verification_emoji_lightbulb">Žiarovka</string>
<string name="verification_emoji_book">Kniha</string>
<string name="verification_emoji_book">Zatvorená kniha</string>
<string name="verification_emoji_pencil">Ceruzka</string>
<string name="verification_emoji_paperclip">Kancelárska sponka</string>
<string name="verification_emoji_paperclip">Sponka na papier</string>
<string name="verification_emoji_scissors">Nožnice</string>
<string name="verification_emoji_lock">Zámok</string>
<string name="verification_emoji_lock">Zatvorená zámka</string>
<string name="verification_emoji_key">Kľúč</string>
<string name="verification_emoji_hammer">Kladivo</string>
<string name="verification_emoji_telephone">Telefón</string>
<string name="verification_emoji_flag">Vlajka</string>
<string name="verification_emoji_train">Vlak</string>
<string name="verification_emoji_flag">Kockovaná zástava</string>
<string name="verification_emoji_train">Rušeň</string>
<string name="verification_emoji_bicycle">Bicykel</string>
<string name="verification_emoji_airplane">Lietadlo</string>
<string name="verification_emoji_rocket">Raketa</string>
<string name="verification_emoji_trophy">Trofej</string>
<string name="verification_emoji_ball">Lopta</string>
<string name="verification_emoji_ball">Futbal</string>
<string name="verification_emoji_guitar">Gitara</string>
<string name="verification_emoji_trumpet">Trúbka</string>
<string name="verification_emoji_bell">Zvonček</string>
<string name="verification_emoji_bell">Zvon</string>
<string name="verification_emoji_anchor">Kotva</string>
<string name="verification_emoji_headphone">Schlúchadlá</string>
<string name="verification_emoji_folder">Priečinok</string>
<string name="verification_emoji_pin">Pin</string>
<string name="verification_emoji_headphone">Slúchadlá</string>
<string name="verification_emoji_folder">Fascikel</string>
<string name="verification_emoji_pin">Špendlík</string>
<string name="initial_sync_start_importing_account">Úvodná synchronizácia:
\nPrebieha import účtu…</string>
@ -173,4 +173,5 @@
<string name="event_status_sending_message">Odosielanie správy…</string>
<string name="clear_timeline_send_queue">Vymazať správy na odoslanie</string>
<string name="notice_room_third_party_revoked_invite">%1$s zamietol pozvanie používateľa %2$s vstúpiť do miestnosti</string>
</resources>

View file

@ -13,7 +13,7 @@
<string name="notice_room_ban">%1$s 封禁了 %2$s</string>
<string name="notice_avatar_url_changed">%1$s 更换了他们的头像</string>
<string name="notice_display_name_set">%1$s 将他们的昵称设置为 %2$s</string>
<string name="notice_display_name_changed_from">%1$s 把他的昵称从 %2$s 改为 %3$s</string>
<string name="notice_display_name_changed_from">%1$s 把他的昵称从 %2$s 改为 %3$s</string>
<string name="notice_display_name_removed">%1$s 移除了他们的昵称 (%2$s)</string>
<string name="notice_room_topic_changed">%1$s 把主题改为: %2$s</string>
<string name="notice_room_name_changed">%1$s 把聊天室名称改为: %2$s</string>
@ -167,4 +167,7 @@
<string name="event_status_sending_message">正在发送消息…</string>
<string name="clear_timeline_send_queue">清除正在发送队列</string>
<string name="notice_room_third_party_revoked_invite">%1$s 撤回了对 %2$s 邀请</string>
<string name="verification_emoji_pin">置顶</string>
</resources>

View file

@ -243,4 +243,18 @@
<string name="event_status_sending_message">Sending message…</string>
<string name="clear_timeline_send_queue">Clear sending queue</string>
<string name="notice_room_invite_no_invitee_with_reason">%1$s\'s invitation. Reason: %2$s</string>
<string name="notice_room_invite_with_reason">%1$s invited %2$s. Reason: %3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s invited you. Reason: %2$s</string>
<string name="notice_room_join_with_reason">%1$s joined. Reason: %2$s</string>
<string name="notice_room_leave_with_reason">%1$s left. Reason: %2$s</string>
<string name="notice_room_reject_with_reason">%1$s rejected the invitation. Reason: %2$s</string>
<string name="notice_room_kick_with_reason">%1$s kicked %2$s. Reason: %3$s</string>
<string name="notice_room_unban_with_reason">%1$s unbanned %2$s. Reason: %3$s</string>
<string name="notice_room_ban_with_reason">%1$s banned %2$s. Reason: %3$s</string>
<string name="notice_room_third_party_invite_with_reason">%1$s sent an invitation to %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s revoked the invitation for %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s accepted the invitation for %2$s. Reason: %3$s</string>
<string name="notice_room_withdraw_with_reason">%1$s withdrew %2$s\'s invitation. Reason: %3$s</string>
</resources>

View file

@ -2,22 +2,23 @@
<resources>
<string name="notice_room_invite_no_invitee_with_reason">%1$s\'s invitation. Reason: %2$s</string>
<string name="notice_room_invite_with_reason">%1$s invited %2$s. Reason: %3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s invited you. Reason: %2$s</string>
<string name="notice_room_join_with_reason">%1$s joined. Reason: %2$s</string>
<string name="notice_room_leave_with_reason">%1$s left. Reason: %2$s</string>
<string name="notice_room_reject_with_reason">%1$s rejected the invitation. Reason: %2$s</string>
<string name="notice_room_kick_with_reason">%1$s kicked %2$s. Reason: %3$s</string>
<string name="notice_room_unban_with_reason">%1$s unbanned %2$s. Reason: %3$s</string>
<string name="notice_room_ban_with_reason">%1$s banned %2$s. Reason: %3$s</string>
<string name="notice_room_third_party_invite_with_reason">%1$s sent an invitation to %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s revoked the invitation for %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s accepted the invitation for %2$s. Reason: %3$s</string>
<string name="notice_room_withdraw_with_reason">%1$s withdrew %2$s\'s invitation. Reason: %3$s</string>
<plurals name="notice_room_aliases_added">
<item quantity="one">%1$s added %2$s as an address for this room.</item>
<item quantity="other">%1$s added %2$s as addresses for this room.</item>
</plurals>
<plurals name="notice_room_aliases_removed">
<item quantity="one">%1$s removed %2$s as an address for this room.</item>
<item quantity="other">%1$s removed %3$s as addresses for this room.</item>
</plurals>
<string name="notice_room_aliases_added_and_removed">%1$s added %2$s and removed %3$s as addresses for this room.</string>
<string name="notice_room_canonical_alias_set">"%1$s set the main address for this room to %2$s."</string>
<string name="notice_room_canonical_alias_unset">"%1$s removed the main address for this room."</string>
<string name="no_network_indicator">There is no network connection right now</string>
<string name="key_verification_request_fallback_message">%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.</string>
</resources>
</resources>

View file

@ -0,0 +1,129 @@
/*
* 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.task
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import java.util.concurrent.Executors
class CoroutineSequencersTest {
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
@Test
fun sequencer_should_run_sequential() {
val sequencer = ChannelCoroutineSequencer<String>()
val results = ArrayList<String>()
val jobs = listOf(
GlobalScope.launch(dispatcher) {
sequencer.post { suspendingMethod("#1") }.also {
results.add(it)
}
},
GlobalScope.launch(dispatcher) {
sequencer.post { suspendingMethod("#2") }.also {
results.add(it)
}
},
GlobalScope.launch(dispatcher) {
sequencer.post { suspendingMethod("#3") }.also {
results.add(it)
}
}
)
runBlocking {
jobs.joinAll()
}
assertEquals(3, results.size)
assertEquals(results[0], "#1")
assertEquals(results[1], "#2")
assertEquals(results[2], "#3")
}
@Test
fun sequencer_should_run_parallel() {
val sequencer1 = ChannelCoroutineSequencer<String>()
val sequencer2 = ChannelCoroutineSequencer<String>()
val sequencer3 = ChannelCoroutineSequencer<String>()
val results = ArrayList<String>()
val jobs = listOf(
GlobalScope.launch(dispatcher) {
sequencer1.post { suspendingMethod("#1") }.also {
results.add(it)
}
},
GlobalScope.launch(dispatcher) {
sequencer2.post { suspendingMethod("#2") }.also {
results.add(it)
}
},
GlobalScope.launch(dispatcher) {
sequencer3.post { suspendingMethod("#3") }.also {
results.add(it)
}
}
)
runBlocking {
jobs.joinAll()
}
assertEquals(3, results.size)
}
@Test
fun sequencer_should_jump_to_next_when_current_job_canceled() {
val sequencer = ChannelCoroutineSequencer<String>()
val results = ArrayList<String>()
val jobs = listOf(
GlobalScope.launch(dispatcher) {
sequencer.post { suspendingMethod("#1") }.also {
results.add(it)
}
},
GlobalScope.launch(dispatcher) {
val result = sequencer.post { suspendingMethod("#2") }.also {
results.add(it)
}
println("Result: $result")
},
GlobalScope.launch(dispatcher) {
sequencer.post { suspendingMethod("#3") }.also {
results.add(it)
}
}
)
// We are canceling the second job
jobs[1].cancel()
runBlocking {
jobs.joinAll()
}
assertEquals(2, results.size)
}
private suspend fun suspendingMethod(name: String): String {
println("BLOCKING METHOD $name STARTS on ${Thread.currentThread().name}")
delay(1000)
println("BLOCKING METHOD $name ENDS on ${Thread.currentThread().name}")
return name
}
}

View file

@ -15,7 +15,7 @@ androidExtensions {
}
ext.versionMajor = 0
ext.versionMinor = 11
ext.versionMinor = 12
ext.versionPatch = 0
static def getGitTimestamp() {
@ -293,6 +293,7 @@ dependencies {
implementation 'me.gujun.android:span:1.7'
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version"
implementation 'com.googlecode.htmlcompressor:htmlcompressor:1.4'
implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.google.android:flexbox:1.1.1'
implementation "androidx.autofill:autofill:$autofill_version"

View file

@ -31,4 +31,8 @@
<issue id="ViewConstructor" severity="error" />
<issue id="UseValueOf" severity="error" />
<!-- Ignore error from HtmlCompressor lib -->
<issue id="InvalidPackage">
<ignore path="**/htmlcompressor-1.4.jar"/>
</issue>
</lint>

View file

@ -4,7 +4,6 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application>
@ -20,10 +19,6 @@
android:enabled="true"
android:exported="false" />
<service
android:name=".fdroid.service.VectorSyncService"
android:exported="false" />
</application>
</manifest>

View file

@ -25,7 +25,7 @@ import android.os.Build
import android.os.PowerManager
import androidx.core.content.ContextCompat
import im.vector.matrix.android.internal.session.sync.job.SyncService
import im.vector.riotx.fdroid.service.VectorSyncService
import im.vector.riotx.core.services.VectorSyncService
import timber.log.Timber
class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
@ -41,14 +41,9 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
val userId = intent.getStringExtra(SyncService.EXTRA_USER_ID)
// This method is called when the BroadcastReceiver is receiving an Intent broadcast.
Timber.d("RestartBroadcastReceiver received intent")
Intent(context, VectorSyncService::class.java).also {
it.putExtra(SyncService.EXTRA_USER_ID, userId)
VectorSyncService.newIntent(context, userId).also {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ContextCompat.startForegroundService(context, it)
} else {
context.startService(it)
}
ContextCompat.startForegroundService(context, it)
} catch (ex: Throwable) {
// TODO
Timber.e(ex)
@ -79,6 +74,7 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
}
fun cancelAlarm(context: Context) {
Timber.v("Cancel alarm")
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager

View file

@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:name=".VectorApplication"
@ -126,6 +127,10 @@
android:name=".core.services.CallService"
android:exported="false" />
<service
android:name=".core.services.VectorSyncService"
android:exported="false" />
<!-- Receivers -->
<!-- Exported false, should only be accessible from this app!! -->

View file

@ -359,6 +359,11 @@ SOFTWARE.
<br/>
Copyright 2018 Kumar Bibek
</li>
<li>
<b>htmlcompressor</b>
<br/>
Copyright 2017 Sergiy Kovalchuk
</li>
</ul>
<pre>
Apache License

View file

@ -116,11 +116,12 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
lastAuthenticatedSession.configureAndStart(pushRuleTriggerListener, sessionListener)
lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
}
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() {
Timber.i("App entered foreground")
FcmHelper.onEnterForeground(appContext)
activeSessionHolder.getSafeActiveSession()?.also {
it.stopAnyBackgroundSync()

View file

@ -38,6 +38,7 @@ import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.HomeRoomListDataSource
import im.vector.riotx.features.home.group.SelectedGroupDataSource
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.VectorHtmlCompressor
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.notifications.*
import im.vector.riotx.features.rageshake.BugReporter
@ -87,6 +88,8 @@ interface VectorComponent {
fun eventHtmlRenderer(): EventHtmlRenderer
fun vectorHtmlCompressor(): VectorHtmlCompressor
fun navigator(): Navigator
fun errorFormatter(): ErrorFormatter

View file

@ -0,0 +1,30 @@
/*
* 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.riotx.core.epoxy
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
/**
* Item of size (0, 0).
* It can be useful to avoid automatic scroll of RecyclerView with Epoxy controller, when the first valuable item changes.
*/
@EpoxyModelClass(layout = R.layout.item_zero)
abstract class ZeroItem : VectorEpoxyModel<ZeroItem.Holder>() {
class Holder : VectorEpoxyHolder()
}

View file

@ -16,11 +16,16 @@
package im.vector.riotx.core.error
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import javax.net.ssl.HttpsURLConnection
import im.vector.riotx.BuildConfig
import timber.log.Timber
fun Throwable.is401(): Boolean {
return (this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& error.code == MatrixError.M_UNAUTHORIZED)
/**
* throw in debug, only log in production. As this method does not always throw, next statement should be a return
*/
fun fatalError(message: String) {
if (BuildConfig.DEBUG) {
error(message)
} else {
Timber.e(message)
}
}

View file

@ -16,24 +16,26 @@
package im.vector.riotx.core.extensions
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.riotx.core.services.VectorSyncService
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.session.SessionListener
import timber.log.Timber
fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener,
fun Session.configureAndStart(context: Context,
pushRuleTriggerListener: PushRuleTriggerListener,
sessionListener: SessionListener) {
open()
addListener(sessionListener)
setFilter(FilterService.FilterPreset.RiotFilter)
Timber.i("Configure and start session for ${this.myUserId}")
val isAtLeastStarted = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
Timber.v("--> is at least started? $isAtLeastStarted")
startSync(isAtLeastStarted)
startSyncing(context)
refreshPushers()
pushRuleTriggerListener.startWithSession(this)
@ -42,6 +44,24 @@ fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener,
// @Inject lateinit var keyRequestHandler: KeyRequestHandler
}
fun Session.startSyncing(context: Context) {
val applicationContext = context.applicationContext
if (!hasAlreadySynced()) {
VectorSyncService.newIntent(applicationContext, myUserId).also {
try {
ContextCompat.startForegroundService(applicationContext, it)
} catch (ex: Throwable) {
// TODO
Timber.e(ex)
}
}
} else {
val isAtLeastStarted = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
Timber.v("--> is at least started? $isAtLeastStarted")
startSync(isAtLeastStarted)
}
}
/**
* Tell is the session has unsaved e2e keys in the backup
*/

View file

@ -222,8 +222,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
override fun onResume() {
super.onResume()
Timber.v("onResume Activity ${this.javaClass.simpleName}")
Timber.i("onResume Activity ${this.javaClass.simpleName}")
configurationViewModel.onActivityResumed()

View file

@ -32,6 +32,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.utils.DimensionConverter
import timber.log.Timber
/**
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)
@ -80,6 +81,11 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
super.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
Timber.i("onResume BottomSheet ${this.javaClass.simpleName}")
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
val dialog = this as? BottomSheetDialog

View file

@ -31,6 +31,7 @@ import butterknife.Unbinder
import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.MvRx
import com.bumptech.glide.util.Util.assertMainThread
import com.google.android.material.snackbar.Snackbar
import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.di.ScreenComponent
@ -104,7 +105,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
@CallSuper
override fun onResume() {
super.onResume()
Timber.v("onResume Fragment ${this.javaClass.simpleName}")
Timber.i("onResume Fragment ${this.javaClass.simpleName}")
}
@CallSuper
@ -167,6 +168,13 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
return this
}
protected fun showErrorInSnackbar(throwable: Throwable) {
vectorBaseActivity.coordinatorLayout?.let {
Snackbar.make(it, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT)
.show()
}
}
/* ==========================================================================================
* Toolbar
* ========================================================================================== */

View file

@ -13,9 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.fdroid.service
package im.vector.riotx.core.services
import android.app.AlarmManager
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
@ -23,10 +25,18 @@ import im.vector.matrix.android.internal.session.sync.job.SyncService
import im.vector.riotx.R
import im.vector.riotx.core.extensions.vectorComponent
import im.vector.riotx.features.notifications.NotificationUtils
import timber.log.Timber
class VectorSyncService : SyncService() {
companion object {
fun newIntent(context: Context, userId: String): Intent {
return Intent(context, VectorSyncService::class.java).also {
it.putExtra(EXTRA_USER_ID, userId)
}
}
}
private lateinit var notificationUtils: NotificationUtils
override fun onCreate() {
@ -34,6 +44,22 @@ class VectorSyncService : SyncService() {
notificationUtils = vectorComponent().notificationUtils()
}
override fun onStart(isInitialSync: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationSubtitleRes = if (isInitialSync) {
R.string.notification_initial_sync
} else {
R.string.notification_listening_for_events
}
val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false)
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
}
}
override fun onRescheduleAsked(userId: String, isInitialSync: Boolean, delay: Long) {
reschedule(userId, delay)
}
override fun onDestroy() {
removeForegroundNotif()
super.onDestroy()
@ -44,16 +70,18 @@ class VectorSyncService : SyncService() {
notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE)
}
/**
* Service is started only in fdroid mode when no FCM is available
* Otherwise it is bounded
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.v("VectorSyncService - onStartCommand ")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notification = notificationUtils.buildForegroundServiceNotification(R.string.notification_listening_for_events, false)
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
private fun reschedule(userId: String, delay: Long) {
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PendingIntent.getForegroundService(this, 0, newIntent(this, userId), 0)
} else {
PendingIntent.getService(this, 0, newIntent(this, userId), 0)
}
val firstMillis = System.currentTimeMillis() + delay
val alarmMgr = getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent)
} else {
alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent)
}
return super.onStartCommand(intent, flags, startId)
}
}

View file

@ -0,0 +1,20 @@
/*
* 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.riotx.core.ui.model
// android.util.Size in API 21+
data class Size(val width: Int, val height: Int)

View file

@ -28,6 +28,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.startSyncing
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.deleteAllFiles
import im.vector.riotx.features.home.HomeActivity
@ -84,11 +85,9 @@ class MainActivity : VectorBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
args = parseArgs()
if (args.clearCredentials || args.isUserLoggedOut) {
clearNotifications()
}
// Handle some wanted cleanup
if (args.clearCache || args.clearCredentials) {
doCleanUp()
@ -116,24 +115,32 @@ class MainActivity : VectorBaseActivity() {
}
private fun doCleanUp() {
val session = sessionHolder.getSafeActiveSession()
if (session == null) {
startNextActivityAndFinish()
return
}
when {
args.clearCredentials -> sessionHolder.getActiveSession().signOut(
args.clearCredentials -> session.signOut(
!args.isUserLoggedOut,
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: success, start app")
sessionHolder.clearActiveSession()
doLocalCleanupAndStart()
doLocalCleanup()
startNextActivityAndFinish()
}
override fun onFailure(failure: Throwable) {
displayError(failure)
}
})
args.clearCache -> sessionHolder.getActiveSession().clearCache(
args.clearCache -> session.clearCache(
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
doLocalCleanupAndStart()
doLocalCleanup()
session.startSyncing(applicationContext)
startNextActivityAndFinish()
}
override fun onFailure(failure: Throwable) {
@ -148,7 +155,7 @@ class MainActivity : VectorBaseActivity() {
Timber.w("Ignoring invalid token global error")
}
private fun doLocalCleanupAndStart() {
private fun doLocalCleanup() {
GlobalScope.launch(Dispatchers.Main) {
// On UI Thread
Glide.get(this@MainActivity).clearMemory()
@ -160,8 +167,6 @@ class MainActivity : VectorBaseActivity() {
deleteAllFiles(this@MainActivity.cacheDir)
}
}
startNextActivityAndFinish()
}
private fun displayError(failure: Throwable) {

View file

@ -18,6 +18,7 @@ package im.vector.riotx.features.attachments
import com.kbeanie.multipicker.api.entity.*
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import timber.log.Timber
fun ChosenContact.toContactAttachment(): ContactAttachment {
return ContactAttachment(
@ -29,6 +30,7 @@ fun ChosenContact.toContactAttachment(): ContactAttachment {
}
fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
@ -40,6 +42,7 @@ fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
}
fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
@ -51,16 +54,17 @@ fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
)
}
fun ChosenFile.mapType(): ContentAttachmentData.Type {
private fun ChosenFile.mapType(): ContentAttachmentData.Type {
return when {
mimeType.startsWith("image/") -> ContentAttachmentData.Type.IMAGE
mimeType.startsWith("video/") -> ContentAttachmentData.Type.VIDEO
mimeType.startsWith("audio/") -> ContentAttachmentData.Type.AUDIO
else -> ContentAttachmentData.Type.FILE
mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE
mimeType?.startsWith("video/") == true -> ContentAttachmentData.Type.VIDEO
mimeType?.startsWith("audio/") == true -> ContentAttachmentData.Type.AUDIO
else -> ContentAttachmentData.Type.FILE
}
}
fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
@ -75,6 +79,7 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
}
fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.riotx.features.autocomplete.user
package im.vector.riotx.features.autocomplete
import android.view.View
import android.widget.ImageView
@ -25,23 +25,27 @@ import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_autocomplete_user)
abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Holder>() {
@EpoxyModelClass(layout = R.layout.item_autocomplete_matrix_item)
abstract class AutocompleteMatrixItem : VectorEpoxyModel<AutocompleteMatrixItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var subName: String? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener)
holder.nameView.text = matrixItem.getBestName()
holder.subNameView.setTextOrHide(subName)
avatarRenderer.render(matrixItem, holder.avatarImageView)
}
class Holder : VectorEpoxyHolder() {
val nameView by bind<TextView>(R.id.userAutocompleteName)
val avatarImageView by bind<ImageView>(R.id.userAutocompleteAvatar)
val nameView by bind<TextView>(R.id.matrixItemAutocompleteName)
val subNameView by bind<TextView>(R.id.matrixItemAutocompleteSubname)
val avatarImageView by bind<ImageView>(R.id.matrixItemAutocompleteAvatar)
}
}

View file

@ -1,91 +0,0 @@
/*
* 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.riotx.features.autocomplete
import android.content.Context
import android.database.DataSetObserver
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyRecyclerView
import com.otaliastudios.autocomplete.AutocompletePresenter
abstract class EpoxyAutocompletePresenter<T>(context: Context) : AutocompletePresenter<T>(context), AutocompleteClickListener<T> {
private var recyclerView: EpoxyRecyclerView? = null
private var clicks: AutocompletePresenter.ClickProvider<T>? = null
private var observer: Observer? = null
override fun registerClickProvider(provider: AutocompletePresenter.ClickProvider<T>) {
this.clicks = provider
}
override fun registerDataSetObserver(observer: DataSetObserver) {
this.observer = Observer(observer)
}
override fun getView(): ViewGroup? {
recyclerView = EpoxyRecyclerView(context).apply {
setController(providesController())
observer?.let {
adapter?.registerAdapterDataObserver(it)
}
itemAnimator = null
}
return recyclerView
}
override fun onViewShown() {}
override fun onViewHidden() {
recyclerView = null
observer = null
}
abstract fun providesController(): EpoxyController
protected fun dispatchLayoutChange() {
observer?.onChanged()
}
override fun onItemClick(t: T) {
clicks?.click(t)
}
private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
root.onChanged()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
root.onChanged()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
root.onChanged()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
root.onChanged()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
root.onChanged()
}
}
}

View file

@ -17,21 +17,28 @@
package im.vector.riotx.features.autocomplete.command
import android.content.Context
import com.airbnb.epoxy.EpoxyController
import im.vector.riotx.features.autocomplete.EpoxyAutocompletePresenter
import androidx.recyclerview.widget.RecyclerView
import com.otaliastudios.autocomplete.RecyclerViewPresenter
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.command.Command
import javax.inject.Inject
class AutocompleteCommandPresenter @Inject constructor(context: Context,
private val controller: AutocompleteCommandController) :
EpoxyAutocompletePresenter<Command>(context) {
RecyclerViewPresenter<Command>(context), AutocompleteClickListener<Command> {
init {
controller.listener = this
}
override fun providesController(): EpoxyController {
return controller
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
// Also remove animation
recyclerView?.itemAnimator = null
return controller.adapter
}
override fun onItemClick(t: Command) {
dispatchClick(t)
}
override fun onQuery(query: CharSequence?) {

View file

@ -0,0 +1,48 @@
/*
* 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.riotx.features.autocomplete.group
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.autocomplete.autocompleteMatrixItem
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
class AutocompleteGroupController @Inject constructor() : TypedEpoxyController<List<GroupSummary>>() {
var listener: AutocompleteClickListener<GroupSummary>? = null
@Inject lateinit var avatarRenderer: AvatarRenderer
override fun buildModels(data: List<GroupSummary>?) {
if (data.isNullOrEmpty()) {
return
}
data.forEach { groupSummary ->
autocompleteMatrixItem {
id(groupSummary.groupId)
matrixItem(groupSummary.toMatrixItem())
avatarRenderer(avatarRenderer)
clickListener { _ ->
listener?.onItemClick(groupSummary)
}
}
}
}
}

View file

@ -0,0 +1,61 @@
/*
* 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.riotx.features.autocomplete.group
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Success
import com.otaliastudios.autocomplete.RecyclerViewPresenter
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import javax.inject.Inject
class AutocompleteGroupPresenter @Inject constructor(context: Context,
private val controller: AutocompleteGroupController
) : RecyclerViewPresenter<GroupSummary>(context), AutocompleteClickListener<GroupSummary> {
var callback: Callback? = null
init {
controller.listener = this
}
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
// Also remove animation
recyclerView?.itemAnimator = null
return controller.adapter
}
override fun onItemClick(t: GroupSummary) {
dispatchClick(t)
}
override fun onQuery(query: CharSequence?) {
callback?.onQueryGroups(query)
}
fun render(groups: Async<List<GroupSummary>>) {
if (groups is Success) {
controller.setData(groups())
}
}
interface Callback {
fun onQueryGroups(query: CharSequence?)
}
}

View file

@ -0,0 +1,49 @@
/*
* 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.riotx.features.autocomplete.room
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.autocomplete.autocompleteMatrixItem
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
class AutocompleteRoomController @Inject constructor() : TypedEpoxyController<List<RoomSummary>>() {
var listener: AutocompleteClickListener<RoomSummary>? = null
@Inject lateinit var avatarRenderer: AvatarRenderer
override fun buildModels(data: List<RoomSummary>?) {
if (data.isNullOrEmpty()) {
return
}
data.forEach { roomSummary ->
autocompleteMatrixItem {
id(roomSummary.roomId)
matrixItem(roomSummary.toMatrixItem())
subName(roomSummary.canonicalAlias)
avatarRenderer(avatarRenderer)
clickListener { _ ->
listener?.onItemClick(roomSummary)
}
}
}
}
}

View file

@ -0,0 +1,61 @@
/*
* 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.riotx.features.autocomplete.room
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Success
import com.otaliastudios.autocomplete.RecyclerViewPresenter
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import javax.inject.Inject
class AutocompleteRoomPresenter @Inject constructor(context: Context,
private val controller: AutocompleteRoomController
) : RecyclerViewPresenter<RoomSummary>(context), AutocompleteClickListener<RoomSummary> {
var callback: Callback? = null
init {
controller.listener = this
}
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
// Also remove animation
recyclerView?.itemAnimator = null
return controller.adapter
}
override fun onItemClick(t: RoomSummary) {
dispatchClick(t)
}
override fun onQuery(query: CharSequence?) {
callback?.onQueryRooms(query)
}
fun render(rooms: Async<List<RoomSummary>>) {
if (rooms is Success) {
controller.setData(rooms())
}
}
interface Callback {
fun onQueryRooms(query: CharSequence?)
}
}

View file

@ -20,6 +20,7 @@ import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.autocomplete.autocompleteMatrixItem
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
@ -34,7 +35,7 @@ class AutocompleteUserController @Inject constructor() : TypedEpoxyController<Li
return
}
data.forEach { user ->
autocompleteUserItem {
autocompleteMatrixItem {
id(user.userId)
matrixItem(user.toMatrixItem())
avatarRenderer(avatarRenderer)

View file

@ -17,16 +17,17 @@
package im.vector.riotx.features.autocomplete.user
import android.content.Context
import com.airbnb.epoxy.EpoxyController
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Success
import com.otaliastudios.autocomplete.RecyclerViewPresenter
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.features.autocomplete.EpoxyAutocompletePresenter
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import javax.inject.Inject
class AutocompleteUserPresenter @Inject constructor(context: Context,
private val controller: AutocompleteUserController
) : EpoxyAutocompletePresenter<User>(context) {
) : RecyclerViewPresenter<User>(context), AutocompleteClickListener<User> {
var callback: Callback? = null
@ -34,8 +35,14 @@ class AutocompleteUserPresenter @Inject constructor(context: Context,
controller.listener = this
}
override fun providesController(): EpoxyController {
return controller
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
// Also remove animation
recyclerView?.itemAnimator = null
return controller.adapter
}
override fun onItemClick(t: User) {
dispatchClick(t)
}
override fun onQuery(query: CharSequence?) {

Some files were not shown because too many files have changed in this diff Show more