Merge branch 'develop' into feature/ons/fix_current_session_ip_address_visibility
This commit is contained in:
commit
61685d3e4a
|
@ -0,0 +1 @@
|
|||
[Poll] When a poll is ended, use /relations API to ensure poll results are correct
|
|
@ -0,0 +1 @@
|
|||
[Session manager] Other sessions list: header should not be sticky
|
|
@ -388,7 +388,13 @@ fun Event.isLocationMessage(): Boolean {
|
|||
}
|
||||
}
|
||||
|
||||
fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START.values || getClearType() in EventType.POLL_END.values
|
||||
fun Event.isPoll(): Boolean = isPollStart() || isPollEnd()
|
||||
|
||||
fun Event.isPollStart(): Boolean = getClearType() in EventType.POLL_START.values
|
||||
|
||||
fun Event.isPollResponse(): Boolean = getClearType() in EventType.POLL_RESPONSE.values
|
||||
|
||||
fun Event.isPollEnd(): Boolean = getClearType() in EventType.POLL_END.values
|
||||
|
||||
fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
|
||||
|
||||
|
|
|
@ -24,10 +24,12 @@ import kotlinx.coroutines.withContext
|
|||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
|
||||
|
@ -85,6 +87,27 @@ internal class EventDecryptor @Inject constructor(
|
|||
return internalDecryptEvent(event, timeline)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an event and save the result in the given event.
|
||||
*
|
||||
* @param event the raw event.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
*/
|
||||
suspend fun decryptEventAndSaveResult(event: Event, timeline: String) {
|
||||
tryOrNull(message = "Unable to decrypt the event") {
|
||||
decryptEvent(event, timeline)
|
||||
}
|
||||
?.let { result ->
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
||||
isSafe = result.isSafe
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an event asynchronously.
|
||||
*
|
||||
|
|
|
@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.room
|
|||
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomStrippedState
|
||||
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
|
||||
|
@ -251,7 +250,7 @@ internal interface RoomAPI {
|
|||
* @param limit max number of Event to retrieve
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}")
|
||||
suspend fun getRelations(
|
||||
suspend fun getRelationsWithEventType(
|
||||
@Path("roomId") roomId: String,
|
||||
@Path("eventId") eventId: String,
|
||||
@Path("relationType") relationType: String,
|
||||
|
@ -262,7 +261,7 @@ internal interface RoomAPI {
|
|||
): RelationsResponse
|
||||
|
||||
/**
|
||||
* Paginate relations for thread events based in normal topological order.
|
||||
* Paginate relations for events based in normal topological order.
|
||||
*
|
||||
* @param roomId the room Id
|
||||
* @param eventId the event Id
|
||||
|
@ -272,10 +271,10 @@ internal interface RoomAPI {
|
|||
* @param limit max number of Event to retrieve
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}")
|
||||
suspend fun getThreadsRelations(
|
||||
suspend fun getRelations(
|
||||
@Path("roomId") roomId: String,
|
||||
@Path("eventId") eventId: String,
|
||||
@Path("relationType") relationType: String = RelationType.THREAD,
|
||||
@Path("relationType") relationType: String,
|
||||
@Query("from") from: String? = null,
|
||||
@Query("to") to: String? = null,
|
||||
@Query("limit") limit: Int? = null
|
||||
|
|
|
@ -99,6 +99,8 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
|
|||
import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.poll.DefaultFetchPollResponseEventsTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
|
||||
|
@ -354,4 +356,7 @@ internal abstract class RoomModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindFetchPollResponseEventsTask(task: DefaultFetchPollResponseEventsTask): FetchPollResponseEventsTask
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.internal.session.room.aggregation.poll
|
||||
|
||||
import io.realm.Realm
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
|
@ -40,9 +41,14 @@ import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSumm
|
|||
import org.matrix.android.sdk.internal.database.query.create
|
||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationProcessor {
|
||||
internal class DefaultPollAggregationProcessor @Inject constructor(
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val fetchPollResponseEventsTask: FetchPollResponseEventsTask,
|
||||
) : PollAggregationProcessor {
|
||||
|
||||
override fun handlePollStartEvent(realm: Realm, event: Event): Boolean {
|
||||
val content = event.getClearContent()?.toModel<MessagePollContent>()
|
||||
|
@ -174,6 +180,10 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
|
|||
aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
|
||||
}
|
||||
|
||||
if (!isLocalEcho) {
|
||||
ensurePollIsFullyAggregated(roomId, pollEventId)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -200,4 +210,20 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
|
|||
eventAnnotationsSummaryEntity.pollResponseSummary = it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that all related votes to a given poll are all retrieved and aggregated.
|
||||
*/
|
||||
private fun ensurePollIsFullyAggregated(
|
||||
roomId: String,
|
||||
pollEventId: String
|
||||
) {
|
||||
taskExecutor.executorScope.launch {
|
||||
val params = FetchPollResponseEventsTask.Params(
|
||||
roomId = roomId,
|
||||
startPollEventId = pollEventId,
|
||||
)
|
||||
fetchPollResponseEventsTask.execute(params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ internal class DefaultFetchEditHistoryTask @Inject constructor(
|
|||
override suspend fun execute(params: FetchEditHistoryTask.Params): List<Event> {
|
||||
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
|
||||
val response = executeRequest(globalErrorReceiver) {
|
||||
roomAPI.getRelations(
|
||||
roomAPI.getRelationsWithEventType(
|
||||
roomId = params.roomId,
|
||||
eventId = params.eventId,
|
||||
relationType = RelationType.REPLACE,
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.room.relation.poll
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.isPollResponse
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
||||
import org.matrix.android.sdk.internal.database.mapper.toEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
||||
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import javax.inject.Inject
|
||||
|
||||
@VisibleForTesting
|
||||
const val FETCH_RELATED_EVENTS_LIMIT = 50
|
||||
|
||||
/**
|
||||
* Task to fetch all the vote events to ensure full aggregation for a given poll.
|
||||
*/
|
||||
internal interface FetchPollResponseEventsTask : Task<FetchPollResponseEventsTask.Params, Result<Unit>> {
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
val startPollEventId: String,
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultFetchPollResponseEventsTask @Inject constructor(
|
||||
private val roomAPI: RoomAPI,
|
||||
private val globalErrorReceiver: GlobalErrorReceiver,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val clock: Clock,
|
||||
private val eventDecryptor: EventDecryptor,
|
||||
) : FetchPollResponseEventsTask {
|
||||
|
||||
override suspend fun execute(params: FetchPollResponseEventsTask.Params): Result<Unit> = runCatching {
|
||||
var nextBatch: String? = fetchAndProcessRelatedEventsFrom(params)
|
||||
|
||||
while (nextBatch?.isNotEmpty() == true) {
|
||||
nextBatch = fetchAndProcessRelatedEventsFrom(params, from = nextBatch)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAndProcessRelatedEventsFrom(params: FetchPollResponseEventsTask.Params, from: String? = null): String? {
|
||||
val response = getRelatedEvents(params, from)
|
||||
|
||||
val filteredEvents = response.chunks
|
||||
.map { decryptEventIfNeeded(it) }
|
||||
.filter { it.isPollResponse() }
|
||||
|
||||
addMissingEventsInDB(params.roomId, filteredEvents)
|
||||
|
||||
return response.nextBatch
|
||||
}
|
||||
|
||||
private suspend fun getRelatedEvents(params: FetchPollResponseEventsTask.Params, from: String? = null): RelationsResponse {
|
||||
return executeRequest(globalErrorReceiver, canRetry = true) {
|
||||
roomAPI.getRelations(
|
||||
roomId = params.roomId,
|
||||
eventId = params.startPollEventId,
|
||||
relationType = RelationType.REFERENCE,
|
||||
from = from,
|
||||
limit = FETCH_RELATED_EVENTS_LIMIT,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun addMissingEventsInDB(roomId: String, events: List<Event>) {
|
||||
monarchy.awaitTransaction { realm ->
|
||||
val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() }
|
||||
if (eventIdsToCheck.isNotEmpty()) {
|
||||
val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId }
|
||||
|
||||
events.filterNot { it.eventId in existingIds }
|
||||
.map { it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = computeLocalTs(it)) }
|
||||
.forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun decryptEventIfNeeded(event: Event): Event {
|
||||
if (event.isEncrypted()) {
|
||||
eventDecryptor.decryptEventAndSaveResult(event, timeline = "")
|
||||
}
|
||||
|
||||
event.ageLocalTs = computeLocalTs(event)
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
private fun computeLocalTs(event: Event) = clock.epochMillis() - (event.unsignedData?.age ?: 0)
|
||||
}
|
|
@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
|
||||
|
@ -102,11 +103,12 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
|||
|
||||
override suspend fun execute(params: FetchThreadTimelineTask.Params): Result {
|
||||
val response = executeRequest(globalErrorReceiver) {
|
||||
roomAPI.getThreadsRelations(
|
||||
roomAPI.getRelations(
|
||||
roomId = params.roomId,
|
||||
eventId = params.rootThreadEventId,
|
||||
relationType = RelationType.THREAD,
|
||||
from = params.from,
|
||||
limit = params.limit
|
||||
limit = params.limit,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.timeline
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||
|
@ -48,18 +46,7 @@ internal class DefaultGetEventTask @Inject constructor(
|
|||
|
||||
// Try to decrypt the Event
|
||||
if (event.isEncrypted()) {
|
||||
tryOrNull(message = "Unable to decrypt the event") {
|
||||
eventDecryptor.decryptEvent(event, "")
|
||||
}
|
||||
?.let { result ->
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
||||
isSafe = result.isSafe
|
||||
)
|
||||
}
|
||||
eventDecryptor.decryptEventAndSaveResult(event, timeline = "")
|
||||
}
|
||||
|
||||
event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0)
|
||||
|
|
|
@ -16,9 +16,13 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.aggregation.poll
|
||||
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.realm.RealmList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeFalse
|
||||
import org.amshove.kluent.shouldBeTrue
|
||||
import org.junit.Before
|
||||
|
@ -34,6 +38,7 @@ import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSumm
|
|||
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_EVENT_ID
|
||||
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_INVALID_POLL_RESPONSE_EVENT
|
||||
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_BROKEN_POLL_REPLACE_EVENT
|
||||
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_CONTENT
|
||||
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_EVENT
|
||||
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REFERENCE_EVENT
|
||||
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REPLACE_EVENT
|
||||
|
@ -43,13 +48,22 @@ import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsT
|
|||
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_ROOM_ID
|
||||
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_TIMELINE_EVENT
|
||||
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_USER_ID_1
|
||||
import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
|
||||
import org.matrix.android.sdk.test.fakes.FakeFetchPollResponseEventsTask
|
||||
import org.matrix.android.sdk.test.fakes.FakeRealm
|
||||
import org.matrix.android.sdk.test.fakes.FakeTaskExecutor
|
||||
import org.matrix.android.sdk.test.fakes.givenEqualTo
|
||||
import org.matrix.android.sdk.test.fakes.givenFindFirst
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DefaultPollAggregationProcessorTest {
|
||||
|
||||
private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor()
|
||||
private val fakeTaskExecutor = FakeTaskExecutor()
|
||||
private val fakeFetchPollResponseEventsTask = FakeFetchPollResponseEventsTask()
|
||||
private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor(
|
||||
taskExecutor = fakeTaskExecutor.instance,
|
||||
fetchPollResponseEventsTask = fakeFetchPollResponseEventsTask
|
||||
)
|
||||
private val realm = FakeRealm()
|
||||
private val session = mockk<Session>()
|
||||
|
||||
|
@ -114,16 +128,28 @@ class DefaultPollAggregationProcessorTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given a poll end event, when processing, then is processed and return true`() {
|
||||
fun `given a poll end event, when processing, then is processed and return true`() = runTest {
|
||||
// Given
|
||||
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
||||
every { fakeTaskExecutor.instance.executorScope } returns this
|
||||
|
||||
// When
|
||||
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
|
||||
|
||||
// Then
|
||||
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() {
|
||||
fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() = runTest {
|
||||
// Given
|
||||
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
||||
every { fakeTaskExecutor.instance.executorScope } returns this
|
||||
|
||||
// When
|
||||
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false)
|
||||
|
||||
// Then
|
||||
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
|
||||
}
|
||||
|
||||
|
@ -135,6 +161,28 @@ class DefaultPollAggregationProcessorTest {
|
|||
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event).shouldBeFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a non local echo poll end event, when is processed, then ensure to aggregate all poll responses`() = runTest {
|
||||
// Given
|
||||
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
||||
val powerLevelsHelper = mockRedactionPowerLevels("another-sender-id", true)
|
||||
val event = A_POLL_END_EVENT.copy(senderId = "another-sender-id")
|
||||
every { fakeTaskExecutor.instance.executorScope } returns this
|
||||
val expectedParams = FetchPollResponseEventsTask.Params(
|
||||
roomId = A_POLL_END_EVENT.roomId.orEmpty(),
|
||||
startPollEventId = A_POLL_END_CONTENT.relatesTo?.eventId.orEmpty(),
|
||||
)
|
||||
|
||||
// When
|
||||
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
coVerify {
|
||||
fakeFetchPollResponseEventsTask.execute(expectedParams)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mockEventAnnotationsSummaryEntity() {
|
||||
realm.givenWhere<EventAnnotationsSummaryEntity>()
|
||||
.givenFindFirst(EventAnnotationsSummaryEntity())
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.room.relation.poll
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkAll
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.isPollResponse
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.database.mapper.toEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
||||
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
|
||||
import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
|
||||
import org.matrix.android.sdk.test.fakes.FakeClock
|
||||
import org.matrix.android.sdk.test.fakes.FakeEventDecryptor
|
||||
import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
|
||||
import org.matrix.android.sdk.test.fakes.FakeMonarchy
|
||||
import org.matrix.android.sdk.test.fakes.FakeRoomApi
|
||||
import org.matrix.android.sdk.test.fakes.givenFindAll
|
||||
import org.matrix.android.sdk.test.fakes.givenIn
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class DefaultFetchPollResponseEventsTaskTest {
|
||||
|
||||
private val fakeRoomAPI = FakeRoomApi()
|
||||
private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver()
|
||||
private val fakeMonarchy = FakeMonarchy()
|
||||
private val fakeClock = FakeClock()
|
||||
private val fakeEventDecryptor = FakeEventDecryptor()
|
||||
|
||||
private val defaultFetchPollResponseEventsTask = DefaultFetchPollResponseEventsTask(
|
||||
roomAPI = fakeRoomAPI.instance,
|
||||
globalErrorReceiver = fakeGlobalErrorReceiver,
|
||||
monarchy = fakeMonarchy.instance,
|
||||
clock = fakeClock,
|
||||
eventDecryptor = fakeEventDecryptor.instance,
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt")
|
||||
mockkStatic("org.matrix.android.sdk.internal.database.mapper.EventMapperKt")
|
||||
mockkStatic("org.matrix.android.sdk.internal.database.query.EventEntityQueriesKt")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a room and a poll when execute then fetch related events and store them in local if needed`() = runTest {
|
||||
// Given
|
||||
val aRoomId = "roomId"
|
||||
val aPollEventId = "eventId"
|
||||
val params = givenTaskParams(roomId = aRoomId, eventId = aPollEventId)
|
||||
val aNextBatchToken = "nextBatch"
|
||||
val anEventId1 = "eventId1"
|
||||
val anEventId2 = "eventId2"
|
||||
val anEventId3 = "eventId3"
|
||||
val anEventId4 = "eventId4"
|
||||
val event1 = givenAnEvent(eventId = anEventId1, isPollResponse = true, isEncrypted = true)
|
||||
val event2 = givenAnEvent(eventId = anEventId2, isPollResponse = true, isEncrypted = true)
|
||||
val event3 = givenAnEvent(eventId = anEventId3, isPollResponse = false, isEncrypted = false)
|
||||
val event4 = givenAnEvent(eventId = anEventId4, isPollResponse = false, isEncrypted = false)
|
||||
val firstEvents = listOf(event1, event2)
|
||||
val secondEvents = listOf(event3, event4)
|
||||
val firstResponse = givenARelationsResponse(events = firstEvents, nextBatch = aNextBatchToken)
|
||||
fakeRoomAPI.givenGetRelationsReturns(from = null, relationsResponse = firstResponse)
|
||||
val secondResponse = givenARelationsResponse(events = secondEvents, nextBatch = null)
|
||||
fakeRoomAPI.givenGetRelationsReturns(from = aNextBatchToken, relationsResponse = secondResponse)
|
||||
fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event1)
|
||||
fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event2)
|
||||
fakeClock.givenEpoch(123)
|
||||
givenExistingEventEntities(eventIdsToCheck = listOf(anEventId1, anEventId2), existingIds = listOf(anEventId1))
|
||||
val eventEntityToSave = EventEntity(eventId = anEventId2)
|
||||
every { event2.toEntity(any(), any(), any()) } returns eventEntityToSave
|
||||
every { eventEntityToSave.copyToRealmOrIgnore(any(), any()) } returns eventEntityToSave
|
||||
|
||||
// When
|
||||
defaultFetchPollResponseEventsTask.execute(params)
|
||||
|
||||
// Then
|
||||
fakeRoomAPI.verifyGetRelations(
|
||||
roomId = params.roomId,
|
||||
eventId = params.startPollEventId,
|
||||
relationType = RelationType.REFERENCE,
|
||||
from = null,
|
||||
limit = FETCH_RELATED_EVENTS_LIMIT
|
||||
)
|
||||
fakeRoomAPI.verifyGetRelations(
|
||||
roomId = params.roomId,
|
||||
eventId = params.startPollEventId,
|
||||
relationType = RelationType.REFERENCE,
|
||||
from = aNextBatchToken,
|
||||
limit = FETCH_RELATED_EVENTS_LIMIT
|
||||
)
|
||||
fakeEventDecryptor.verifyDecryptEventAndSaveResult(event1, timeline = "")
|
||||
fakeEventDecryptor.verifyDecryptEventAndSaveResult(event2, timeline = "")
|
||||
// Check we save in DB the event2 which is a non stored poll response
|
||||
verify {
|
||||
event2.toEntity(aRoomId, SendState.SYNCED, any())
|
||||
eventEntityToSave.copyToRealmOrIgnore(fakeMonarchy.fakeRealm.instance, EventInsertType.PAGINATION)
|
||||
}
|
||||
}
|
||||
|
||||
private fun givenTaskParams(roomId: String, eventId: String) = FetchPollResponseEventsTask.Params(
|
||||
roomId = roomId,
|
||||
startPollEventId = eventId,
|
||||
)
|
||||
|
||||
private fun givenARelationsResponse(events: List<Event>, nextBatch: String?): RelationsResponse {
|
||||
return RelationsResponse(
|
||||
chunks = events,
|
||||
nextBatch = nextBatch,
|
||||
prevBatch = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun givenAnEvent(
|
||||
eventId: String,
|
||||
isPollResponse: Boolean,
|
||||
isEncrypted: Boolean,
|
||||
): Event {
|
||||
val event = mockk<Event>(relaxed = true)
|
||||
every { event.eventId } returns eventId
|
||||
every { event.isPollResponse() } returns isPollResponse
|
||||
every { event.isEncrypted() } returns isEncrypted
|
||||
return event
|
||||
}
|
||||
|
||||
private fun givenExistingEventEntities(eventIdsToCheck: List<String>, existingIds: List<String>) {
|
||||
val eventEntities = existingIds.map { EventEntity(eventId = it) }
|
||||
fakeMonarchy.givenWhere<EventEntity>()
|
||||
.givenIn(EventEntityFields.EVENT_ID, eventIdsToCheck)
|
||||
.givenFindAll(eventEntities)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.test.fakes
|
||||
|
||||
import io.mockk.coJustRun
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
||||
|
||||
internal class FakeEventDecryptor {
|
||||
val instance: EventDecryptor = mockk()
|
||||
|
||||
fun givenDecryptEventAndSaveResultSuccess(event: Event) {
|
||||
coJustRun { instance.decryptEventAndSaveResult(event, any()) }
|
||||
}
|
||||
|
||||
fun verifyDecryptEventAndSaveResult(event: Event, timeline: String) {
|
||||
coVerify { instance.decryptEventAndSaveResult(event, timeline) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.test.fakes
|
||||
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
|
||||
|
||||
class FakeFetchPollResponseEventsTask : FetchPollResponseEventsTask by mockk(relaxed = true)
|
|
@ -109,6 +109,14 @@ inline fun <reified T : RealmModel> RealmQuery<T>.givenLessThan(
|
|||
return this
|
||||
}
|
||||
|
||||
inline fun <reified T : RealmModel> RealmQuery<T>.givenIn(
|
||||
fieldName: String,
|
||||
values: List<String>,
|
||||
): RealmQuery<T> {
|
||||
every { `in`(fieldName, values.toTypedArray()) } returns this
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.test.fakes
|
||||
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
|
||||
|
||||
internal class FakeRoomApi {
|
||||
|
||||
val instance: RoomAPI = mockk()
|
||||
|
||||
fun givenGetRelationsReturns(
|
||||
from: String?,
|
||||
relationsResponse: RelationsResponse,
|
||||
) {
|
||||
coEvery {
|
||||
instance.getRelations(
|
||||
roomId = any(),
|
||||
eventId = any(),
|
||||
relationType = any(),
|
||||
from = from,
|
||||
limit = any()
|
||||
)
|
||||
} returns relationsResponse
|
||||
}
|
||||
|
||||
fun verifyGetRelations(
|
||||
roomId: String,
|
||||
eventId: String,
|
||||
relationType: String,
|
||||
from: String?,
|
||||
limit: Int,
|
||||
) {
|
||||
coVerify {
|
||||
instance.getRelations(
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
relationType = relationType,
|
||||
from = from,
|
||||
limit = limit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -93,7 +93,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
override fun renderLiveIndicator(holder: Holder) {
|
||||
when {
|
||||
voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED -> renderNoLiveIndicator(holder)
|
||||
voiceBroadcastState == VoiceBroadcastState.PAUSED || !player.isLiveListening -> renderPausedLiveIndicator(holder)
|
||||
voiceBroadcastState == VoiceBroadcastState.PAUSED -> renderPausedLiveIndicator(holder)
|
||||
else -> renderPlayingLiveIndicator(holder)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,7 +130,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
||||
}
|
||||
listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE)
|
||||
listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast && isLiveListening)
|
||||
listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast)
|
||||
}
|
||||
|
||||
override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) {
|
||||
|
@ -373,11 +373,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onLiveListeningChanged(isLiveListening: Boolean) {
|
||||
currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId ->
|
||||
// Notify live mode change to all the listeners attached to the current voice broadcast id
|
||||
listeners[voiceBroadcastId]?.forEach { listener -> listener.onLiveModeChanged(isLiveListening) }
|
||||
}
|
||||
|
||||
// Live has ended and last chunk has been reached, we can stop the playback
|
||||
if (!isLiveListening && playingState == State.BUFFERING && playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence) {
|
||||
stop()
|
||||
|
|
|
@ -1,120 +1,132 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/otherSessionsNotFoundLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="72dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/otherSessionsNotFoundTextView"
|
||||
style="@style/TextAppearance.Vector.Body"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="@string/device_manager_other_sessions_no_verified_sessions_found" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/otherSessionsClearFilterButton"
|
||||
style="@style/Widget.Vector.Button.Text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:gravity="start"
|
||||
android:padding="0dp"
|
||||
android:text="@string/device_manager_other_sessions_clear_filter" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<im.vector.app.features.settings.devices.v2.list.OtherSessionsView
|
||||
android:id="@+id/deviceListOtherSessions"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBarLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/otherSessionsToolbar"
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:navigationIcon="@drawable/ic_back_24dp"
|
||||
app:title="@string/device_manager_sessions_other_title">
|
||||
android:layout_height="match_parent"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed"
|
||||
app:titleEnabled="false"
|
||||
app:toolbarId="@id/otherSessionsToolbar">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/otherSessionsFilterFrameLayout"
|
||||
android:layout_width="wrap_content"
|
||||
<im.vector.app.features.settings.devices.v2.list.SessionsListHeaderView
|
||||
android:id="@+id/deviceListHeaderOtherSessions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:padding="8dp">
|
||||
android:layout_marginTop="?attr/actionBarSize"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:layout_collapseMode="parallax"
|
||||
app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description"
|
||||
app:sessionsListHeaderHasLearnMoreLink="false"
|
||||
app:sessionsListHeaderTitle="" />
|
||||
|
||||
<ImageView
|
||||
<im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsSecurityRecommendationView
|
||||
android:id="@+id/otherSessionsSecurityRecommendationView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="?attr/actionBarSize"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:paddingTop="20dp"
|
||||
android:visibility="gone"
|
||||
app:layout_collapseMode="parallax"
|
||||
app:otherSessionsRecommendationDescription="@string/device_manager_other_sessions_recommendation_description_unverified"
|
||||
app:otherSessionsRecommendationImageBackgroundTint="@color/shield_color_warning_background"
|
||||
app:otherSessionsRecommendationImageResource="@drawable/ic_shield_warning_no_border"
|
||||
app:otherSessionsRecommendationTitle="@string/device_manager_other_sessions_recommendation_title_unverified"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/otherSessionsToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_collapseMode="pin"
|
||||
app:navigationIcon="@drawable/ic_back_24dp"
|
||||
app:title="@string/device_manager_sessions_other_title">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/otherSessionsFilterFrameLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/a11y_device_manager_filter"
|
||||
android:src="@drawable/ic_filter" />
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:padding="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/otherSessionsFilterBadgeImageView"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/circle_with_transparent_border" />
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/a11y_device_manager_filter"
|
||||
android:src="@drawable/ic_filter" />
|
||||
|
||||
</FrameLayout>
|
||||
<ImageView
|
||||
android:id="@+id/otherSessionsFilterBadgeImageView"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/circle_with_transparent_border" />
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
</FrameLayout>
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
<im.vector.app.features.settings.devices.v2.list.SessionsListHeaderView
|
||||
android:id="@+id/deviceListHeaderOtherSessions"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/appBarLayout"
|
||||
app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description"
|
||||
app:sessionsListHeaderHasLearnMoreLink="false"
|
||||
app:sessionsListHeaderTitle="" />
|
||||
|
||||
<im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsSecurityRecommendationView
|
||||
android:id="@+id/otherSessionsSecurityRecommendationView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/deviceListHeaderOtherSessions"
|
||||
app:otherSessionsRecommendationDescription="@string/device_manager_other_sessions_recommendation_description_unverified"
|
||||
app:otherSessionsRecommendationImageBackgroundTint="@color/shield_color_warning_background"
|
||||
app:otherSessionsRecommendationImageResource="@drawable/ic_shield_warning_no_border"
|
||||
app:otherSessionsRecommendationTitle="@string/device_manager_other_sessions_recommendation_title_unverified"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/otherSessionsNotFoundLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="72dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/otherSessionsSecurityRecommendationView">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/otherSessionsNotFoundTextView"
|
||||
style="@style/TextAppearance.Vector.Body"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="@string/device_manager_other_sessions_no_verified_sessions_found" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/otherSessionsClearFilterButton"
|
||||
style="@style/Widget.Vector.Button.Text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:gravity="start"
|
||||
android:padding="0dp"
|
||||
android:text="@string/device_manager_other_sessions_clear_filter" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<im.vector.app.features.settings.devices.v2.list.OtherSessionsView
|
||||
android:id="@+id/deviceListOtherSessions"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="32dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/otherSessionsSecurityRecommendationView" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
Loading…
Reference in New Issue