diff --git a/changelog.d/5711.feature b/changelog.d/5711.feature new file mode 100644 index 0000000000..76c6b23b69 --- /dev/null +++ b/changelog.d/5711.feature @@ -0,0 +1 @@ +Live Location Sharing - Attach location data to beacon info state event \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt index a4551d462e..a3faee0568 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt @@ -20,6 +20,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.room.model.message.LocationAsset import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType +import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent @JsonClass(generateAdapter = true) data class LiveLocationBeaconContent( @@ -37,7 +38,12 @@ data class LiveLocationBeaconContent( * Live location asset type. */ @Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset = LocationAsset(LocationAssetType.SELF), - @Json(name = "m.asset") val locationAsset: LocationAsset? = null + @Json(name = "m.asset") val locationAsset: LocationAsset? = null, + + /** + * Client side tracking of the last location + */ + var lastLocationContent: MessageLiveLocationContent? = null ) { fun getBestBeaconInfo() = beaconInfo ?: unstableBeaconInfo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLiveLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLiveLocationContent.kt index e5de46b3ef..548dc85369 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLiveLocationContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLiveLocationContent.kt @@ -42,11 +42,11 @@ data class MessageLiveLocationContent( /** * Exact time that the data in the event refers to (milliseconds since the UNIX epoch) */ - @Json(name = "org.matrix.msc3488.ts") val unstableTs: Long? = null, - @Json(name = "m.ts") val ts: Long? = null + @Json(name = "org.matrix.msc3488.ts") val unstableTimestampAsMilliseconds: Long? = null, + @Json(name = "m.ts") val timestampAsMilliseconds: Long? = null ) : MessageContent { fun getBestLocationInfo() = locationInfo ?: unstableLocationInfo - fun getBestTs() = ts ?: unstableTs + fun getBestTimestampAsMilliseconds() = timestampAsMilliseconds ?: unstableTimestampAsMilliseconds } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 531dea1d5a..6e71a5393b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -87,6 +87,8 @@ import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationMan import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor +import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.DefaultLiveLocationAggregationProcessor +import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor @@ -390,4 +392,7 @@ internal abstract class SessionModule { @Binds abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor + + @Binds + abstract fun bindLiveLocationAggregationProcessor(processor: DefaultLiveLocationAggregationProcessor): LiveLocationAggregationProcessor } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 8bbe3a9ac6..27a70b27b4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.model.VoteInfo import org.matrix.android.sdk.api.session.room.model.VoteSummary import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent +import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent @@ -64,6 +65,7 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import timber.log.Timber import javax.inject.Inject @@ -72,7 +74,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( @UserId private val userId: String, private val stateEventDataSource: StateEventDataSource, @SessionId private val sessionId: String, - private val sessionManager: SessionManager + private val sessionManager: SessionManager, + private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor ) : EventInsertLiveProcessor { private val allowedTypes = listOf( @@ -88,7 +91,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( // EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_KEY, EventType.ENCRYPTED - ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END + ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END + EventType.BEACON_LOCATION_DATA override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { return allowedTypes.contains(eventType) @@ -103,12 +106,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") when (event.type) { - EventType.REACTION -> { + EventType.REACTION -> { // we got a reaction!! Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") handleReaction(realm, event, roomId, isLocalEcho) } - EventType.MESSAGE -> { + EventType.MESSAGE -> { if (event.unsignedData?.relations?.annotations != null) { Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}") handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations) @@ -134,7 +137,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_KEY -> { + EventType.KEY_VERIFICATION_KEY -> { Timber.v("## SAS REF in room $roomId for event ${event.eventId}") event.content.toModel()?.relatesTo?.let { if (it.type == RelationType.REFERENCE && it.eventId != null) { @@ -143,7 +146,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } - EventType.ENCRYPTED -> { + EventType.ENCRYPTED -> { // Relation type is in clear val encryptedEventContent = event.content.toModel() if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE || @@ -169,22 +172,27 @@ internal class EventRelationsAggregationProcessor @Inject constructor( EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_KEY -> { + EventType.KEY_VERIFICATION_KEY -> { Timber.v("## SAS REF in room $roomId for event ${event.eventId}") encryptedEventContent.relatesTo.eventId?.let { handleVerification(realm, event, roomId, isLocalEcho, it) } } - in EventType.POLL_RESPONSE -> { + in EventType.POLL_RESPONSE -> { event.getClearContent().toModel(catchError = true)?.let { handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId) } } - in EventType.POLL_END -> { + in EventType.POLL_END -> { event.content.toModel(catchError = true)?.let { handleEndPoll(realm, event, it, roomId, isLocalEcho) } } + in EventType.BEACON_LOCATION_DATA -> { + event.content.toModel(catchError = true)?.let { + liveLocationAggregationProcessor.handleLiveLocation(realm, event, it, roomId, isLocalEcho) + } + } } } else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) { // Reaction @@ -205,7 +213,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( // } // } } - EventType.REDACTION -> { + EventType.REDACTION -> { val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } ?: return when (eventToPrune.type) { @@ -225,7 +233,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } } - in EventType.POLL_START -> { + in EventType.POLL_START -> { val content: MessagePollContent? = event.content.toModel() if (content?.relatesTo?.type == RelationType.REPLACE) { Timber.v("###REPLACE in room $roomId for event ${event.eventId}") @@ -233,17 +241,22 @@ internal class EventRelationsAggregationProcessor @Inject constructor( handleReplace(realm, event, content, roomId, isLocalEcho) } } - in EventType.POLL_RESPONSE -> { + in EventType.POLL_RESPONSE -> { event.content.toModel(catchError = true)?.let { handleResponse(realm, event, it, roomId, isLocalEcho) } } - in EventType.POLL_END -> { + in EventType.POLL_END -> { event.content.toModel(catchError = true)?.let { handleEndPoll(realm, event, it, roomId, isLocalEcho) } } - else -> Timber.v("UnHandled event ${event.eventId}") + in EventType.BEACON_LOCATION_DATA -> { + event.content.toModel(catchError = true)?.let { + liveLocationAggregationProcessor.handleLiveLocation(realm, event, it, roomId, isLocalEcho) + } + } + else -> Timber.v("UnHandled event ${event.eventId}") } } catch (t: Throwable) { Timber.e(t, "## Should not happen ") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt new file mode 100644 index 0000000000..2af536a4a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt @@ -0,0 +1,84 @@ +/* + * Copyright 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.aggregation.livelocation + +import io.realm.Realm +import org.matrix.android.sdk.api.extensions.orFalse +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.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationBeaconContent +import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.query.getOrNull +import timber.log.Timber +import javax.inject.Inject + +internal class DefaultLiveLocationAggregationProcessor @Inject constructor() : LiveLocationAggregationProcessor { + + override fun handleLiveLocation(realm: Realm, event: Event, content: MessageLiveLocationContent, roomId: String, isLocalEcho: Boolean) { + val locationSenderId = event.senderId ?: return + + // We shouldn't process local echos + if (isLocalEcho) { + return + } + + // A beacon info state event has to be sent before sending location + var beaconInfoEntity: CurrentStateEventEntity? = null + val eventTypesIterator = EventType.STATE_ROOM_BEACON_INFO.iterator() + while (beaconInfoEntity == null && eventTypesIterator.hasNext()) { + beaconInfoEntity = CurrentStateEventEntity.getOrNull(realm, roomId, locationSenderId, eventTypesIterator.next()) + } + + if (beaconInfoEntity == null) { + Timber.v("## LIVE LOCATION. There is not any beacon info which should be emitted before sending location updates") + return + } + val beaconInfoContent = ContentMapper.map(beaconInfoEntity.root?.content)?.toModel(catchError = true) + if (beaconInfoContent == null) { + Timber.v("## LIVE LOCATION. Beacon info content is invalid") + return + } + + // Check if live location is ended + if (!beaconInfoContent.getBestBeaconInfo()?.isLive.orFalse()) { + Timber.v("## LIVE LOCATION. Beacon info is not live anymore") + return + } + + // Check if beacon info is outdated + if (isBeaconInfoOutdated(beaconInfoContent, content)) { + Timber.v("## LIVE LOCATION. Beacon info has timeout") + return + } + + // Update last location info of the beacon state event + beaconInfoContent.lastLocationContent = content + beaconInfoEntity.root?.content = ContentMapper.map(beaconInfoContent.toContent()) + } + + private fun isBeaconInfoOutdated(beaconInfoContent: LiveLocationBeaconContent, + liveLocationContent: MessageLiveLocationContent): Boolean { + val beaconInfoStartTime = beaconInfoContent.getBestTimestampAsMilliseconds() ?: 0 + val liveLocationEventTime = liveLocationContent.getBestTimestampAsMilliseconds() ?: 0 + val timeout = beaconInfoContent.getBestBeaconInfo()?.timeout ?: 0 + return liveLocationEventTime - beaconInfoStartTime > timeout + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt new file mode 100644 index 0000000000..447eed21b0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt @@ -0,0 +1,29 @@ +/* + * Copyright 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.aggregation.livelocation + +import io.realm.Realm +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent + +interface LiveLocationAggregationProcessor { + fun handleLiveLocation(realm: Realm, + event: Event, + content: MessageLiveLocationContent, + roomId: String, + isLocalEcho: Boolean) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index d7d74ca601..ba98bfc25b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -256,7 +256,7 @@ internal class LocalEchoEventFactory @Inject constructor( eventId = beaconInfoEventId ), unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri), - unstableTs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()), + unstableTimestampAsMilliseconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()), ) val localId = LocalEcho.createLocalEchoId() return Event(