mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 05:31:21 +03:00
Merge branch 'develop' into cross_signing
This commit is contained in:
commit
0997d9abf4
172 changed files with 3470 additions and 1769 deletions
34
CHANGES.md
34
CHANGES.md
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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>)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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) },
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
),
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 *****************************************************************************
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>()
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,5 @@ internal enum class TaskThread {
|
|||
IO,
|
||||
CALLER,
|
||||
CRYPTO,
|
||||
SYNC,
|
||||
DM_VERIF
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,5 @@ internal data class MatrixCoroutineDispatchers(
|
|||
val computation: CoroutineDispatcher,
|
||||
val main: CoroutineDispatcher,
|
||||
val crypto: CoroutineDispatcher,
|
||||
val sync: CoroutineDispatcher,
|
||||
val dmVerif: CoroutineDispatcher
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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!! -->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
30
vector/src/main/java/im/vector/riotx/core/epoxy/ZeroItem.kt
Normal file
30
vector/src/main/java/im/vector/riotx/core/epoxy/ZeroItem.kt
Normal 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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
* ========================================================================================== */
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
20
vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt
Normal file
20
vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt
Normal 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)
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue