Merge pull request #6304 from vector-im/feature/mna/location-sharing-service-api

[SDK] Improve location sharing service api (PSF-1004)
This commit is contained in:
Maxime NATUREL 2022-06-20 13:48:13 +02:00 committed by GitHub
commit 0948cab31f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1150 additions and 146 deletions

1
changelog.d/5864.sdk Normal file
View file

@ -0,0 +1 @@
Group all location sharing related API into LocationSharingService

View file

@ -18,10 +18,45 @@ package org.matrix.android.sdk.api.session.room.location
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
import org.matrix.android.sdk.api.util.Cancelable
/** /**
* Manage all location sharing related features. * Manage all location sharing related features.
*/ */
interface LocationSharingService { interface LocationSharingService {
/**
* Send a static location event to the room.
* @param latitude required latitude of the location
* @param longitude required longitude of the location
* @param uncertainty Accuracy of the location in meters
* @param isUserLocation indicates whether the location data corresponds to the user location or not (pinned location)
*/
suspend fun sendStaticLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable
/**
* Send a live location event to the room.
* To get the beacon info event id, [startLiveLocationShare] must be called before sending live location updates.
* @param beaconInfoEventId event id of the initial beacon info state event
* @param latitude required latitude of the location
* @param longitude required longitude of the location
* @param uncertainty Accuracy of the location in meters
*/
suspend fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable
/**
* Starts sharing live location in the room.
* @param timeoutMillis timeout of the live in milliseconds
* @return the id of the created beacon info event
*/
suspend fun startLiveLocationShare(timeoutMillis: Long): String
/**
* Stops sharing live location in the room.
*/
suspend fun stopLiveLocationShare()
/**
* Returns a LiveData on the list of current running live location shares.
*/
fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>> fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>>
} }

View file

@ -142,24 +142,6 @@ interface SendService {
*/ */
fun resendMediaMessage(localEcho: TimelineEvent): Cancelable fun resendMediaMessage(localEcho: TimelineEvent): Cancelable
/**
* Send a location event to the room.
* @param latitude required latitude of the location
* @param longitude required longitude of the location
* @param uncertainty Accuracy of the location in meters
* @param isUserLocation indicates whether the location data corresponds to the user location or not
*/
fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable
/**
* Send a live location event to the room. beacon_info state event has to be sent before sending live location updates.
* @param beaconInfoEventId event id of the initial beacon info state event
* @param latitude required latitude of the location
* @param longitude required longitude of the location
* @param uncertainty Accuracy of the location in meters
*/
fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable
/** /**
* Remove this failed message from the timeline. * Remove this failed message from the timeline.
* @param localEcho the unsent local echo * @param localEcho the unsent local echo

View file

@ -66,19 +66,6 @@ interface StateService {
*/ */
suspend fun deleteAvatar() suspend fun deleteAvatar()
/**
* Stops sharing live location in the room.
* @param userId user id
*/
suspend fun stopLiveLocation(userId: String)
/**
* Returns beacon info state event of a user.
* @param userId user id who is sharing location
* @param filterOnlyLive filters only ongoing live location sharing beacons if true else ended event is included
*/
suspend fun getLiveLocationBeaconInfo(userId: String, filterOnlyLive: Boolean): Event?
/** /**
* Send a state event to the room. * Send a state event to the room.
* @param eventType The type of event to send. * @param eventType The type of event to send.

View file

@ -16,15 +16,17 @@
package org.matrix.android.sdk.internal.database.mapper package org.matrix.android.sdk.internal.database.mapper
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
import javax.inject.Inject import javax.inject.Inject
internal class LiveLocationShareAggregatedSummaryMapper @Inject constructor() { internal class LiveLocationShareAggregatedSummaryMapper @Inject constructor() :
Monarchy.Mapper<LiveLocationShareAggregatedSummary, LiveLocationShareAggregatedSummaryEntity> {
fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary { override fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary {
return LiveLocationShareAggregatedSummary( return LiveLocationShareAggregatedSummary(
userId = entity.userId, userId = entity.userId,
isActive = entity.isActive, isActive = entity.isActive,

View file

@ -51,6 +51,14 @@ import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDire
import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask
import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask
import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask
import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask
import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask
import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask
import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask
import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask
import org.matrix.android.sdk.internal.session.room.location.StopLiveLocationShareTask
import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.membership.admin.DefaultMembershipAdminTask import org.matrix.android.sdk.internal.session.room.membership.admin.DefaultMembershipAdminTask
@ -299,4 +307,16 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindFetchThreadSummariesTask(task: DefaultFetchThreadSummariesTask): FetchThreadSummariesTask abstract fun bindFetchThreadSummariesTask(task: DefaultFetchThreadSummariesTask): FetchThreadSummariesTask
@Binds
abstract fun bindStartLiveLocationShareTask(task: DefaultStartLiveLocationShareTask): StartLiveLocationShareTask
@Binds
abstract fun bindStopLiveLocationShareTask(task: DefaultStopLiveLocationShareTask): StopLiveLocationShareTask
@Binds
abstract fun bindSendStaticLocationTask(task: DefaultSendStaticLocationTask): SendStaticLocationTask
@Binds
abstract fun bindSendLiveLocationTask(task: DefaultSendLiveLocationTask): SendLiveLocationTask
} }

View file

@ -23,15 +23,19 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.location.LocationSharingService
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.query.findRunningLiveInRoom import org.matrix.android.sdk.internal.database.query.findRunningLiveInRoom
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
// TODO add unit tests
internal class DefaultLocationSharingService @AssistedInject constructor( internal class DefaultLocationSharingService @AssistedInject constructor(
@Assisted private val roomId: String, @Assisted private val roomId: String,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val sendStaticLocationTask: SendStaticLocationTask,
private val sendLiveLocationTask: SendLiveLocationTask,
private val startLiveLocationShareTask: StartLiveLocationShareTask,
private val stopLiveLocationShareTask: StopLiveLocationShareTask,
private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper, private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper,
) : LocationSharingService { ) : LocationSharingService {
@ -40,10 +44,47 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
fun create(roomId: String): DefaultLocationSharingService fun create(roomId: String): DefaultLocationSharingService
} }
override suspend fun sendStaticLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable {
val params = SendStaticLocationTask.Params(
roomId = roomId,
latitude = latitude,
longitude = longitude,
uncertainty = uncertainty,
isUserLocation = isUserLocation,
)
return sendStaticLocationTask.execute(params)
}
override suspend fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable {
val params = SendLiveLocationTask.Params(
beaconInfoEventId = beaconInfoEventId,
roomId = roomId,
latitude = latitude,
longitude = longitude,
uncertainty = uncertainty,
)
return sendLiveLocationTask.execute(params)
}
override suspend fun startLiveLocationShare(timeoutMillis: Long): String {
val params = StartLiveLocationShareTask.Params(
roomId = roomId,
timeoutMillis = timeoutMillis
)
return startLiveLocationShareTask.execute(params)
}
override suspend fun stopLiveLocationShare() {
val params = StopLiveLocationShareTask.Params(
roomId = roomId,
)
return stopLiveLocationShareTask.execute(params)
}
override fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>> { override fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) }, { LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) },
{ liveLocationShareAggregatedSummaryMapper.map(it) } liveLocationShareAggregatedSummaryMapper
) )
} }
} }

View file

@ -0,0 +1,51 @@
/*
* 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.location
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface SendLiveLocationTask : Task<SendLiveLocationTask.Params, Cancelable> {
data class Params(
val roomId: String,
val beaconInfoEventId: String,
val latitude: Double,
val longitude: Double,
val uncertainty: Double?,
)
}
internal class DefaultSendLiveLocationTask @Inject constructor(
private val localEchoEventFactory: LocalEchoEventFactory,
private val eventSenderProcessor: EventSenderProcessor,
) : SendLiveLocationTask {
override suspend fun execute(params: SendLiveLocationTask.Params): Cancelable {
val event = localEchoEventFactory.createLiveLocationEvent(
beaconInfoEventId = params.beaconInfoEventId,
roomId = params.roomId,
latitude = params.latitude,
longitude = params.longitude,
uncertainty = params.uncertainty,
)
localEchoEventFactory.createLocalEcho(event)
return eventSenderProcessor.postEvent(event)
}
}

View file

@ -0,0 +1,51 @@
/*
* 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.location
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface SendStaticLocationTask : Task<SendStaticLocationTask.Params, Cancelable> {
data class Params(
val roomId: String,
val latitude: Double,
val longitude: Double,
val uncertainty: Double?,
val isUserLocation: Boolean
)
}
internal class DefaultSendStaticLocationTask @Inject constructor(
private val localEchoEventFactory: LocalEchoEventFactory,
private val eventSenderProcessor: EventSenderProcessor,
) : SendStaticLocationTask {
override suspend fun execute(params: SendStaticLocationTask.Params): Cancelable {
val event = localEchoEventFactory.createStaticLocationEvent(
roomId = params.roomId,
latitude = params.latitude,
longitude = params.longitude,
uncertainty = params.uncertainty,
isUserLocation = params.isUserLocation
)
localEchoEventFactory.createLocalEcho(event)
return eventSenderProcessor.postEvent(event)
}
}

View file

@ -0,0 +1,56 @@
/*
* 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.location
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.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.time.Clock
import javax.inject.Inject
internal interface StartLiveLocationShareTask : Task<StartLiveLocationShareTask.Params, String> {
data class Params(
val roomId: String,
val timeoutMillis: Long,
)
}
internal class DefaultStartLiveLocationShareTask @Inject constructor(
@UserId private val userId: String,
private val clock: Clock,
private val sendStateTask: SendStateTask,
) : StartLiveLocationShareTask {
override suspend fun execute(params: StartLiveLocationShareTask.Params): String {
val beaconContent = MessageBeaconInfoContent(
timeout = params.timeoutMillis,
isLive = true,
unstableTimestampMillis = clock.epochMillis()
).toContent()
val eventType = EventType.STATE_ROOM_BEACON_INFO.first()
val sendStateTaskParams = SendStateTask.Params(
roomId = params.roomId,
stateKey = userId,
eventType = eventType,
body = beaconContent
)
return sendStateTask.executeRetry(sendStateTaskParams, 3)
}
}

View file

@ -0,0 +1,71 @@
/*
* 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.location
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
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.message.MessageBeaconInfoContent
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface StopLiveLocationShareTask : Task<StopLiveLocationShareTask.Params, Unit> {
data class Params(
val roomId: String,
)
}
internal class DefaultStopLiveLocationShareTask @Inject constructor(
@UserId private val userId: String,
private val sendStateTask: SendStateTask,
private val stateEventDataSource: StateEventDataSource,
) : StopLiveLocationShareTask {
override suspend fun execute(params: StopLiveLocationShareTask.Params) {
val beaconInfoStateEvent = getLiveLocationBeaconInfoForUser(userId, params.roomId) ?: return
val stateKey = beaconInfoStateEvent.stateKey ?: return
val content = beaconInfoStateEvent.getClearContent()?.toModel<MessageBeaconInfoContent>() ?: return
val updatedContent = content.copy(isLive = false).toContent()
val sendStateTaskParams = SendStateTask.Params(
roomId = params.roomId,
stateKey = stateKey,
eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
body = updatedContent
)
sendStateTask.executeRetry(sendStateTaskParams, 3)
}
private fun getLiveLocationBeaconInfoForUser(userId: String, roomId: String): Event? {
return EventType.STATE_ROOM_BEACON_INFO
.mapNotNull {
stateEventDataSource.getStateEvent(
roomId = roomId,
eventType = it,
stateKey = QueryStringValue.Equals(userId)
)
}
.firstOrNull { beaconInfoEvent ->
beaconInfoEvent.getClearContent()?.toModel<MessageBeaconInfoContent>()?.isLive.orFalse()
}
}
}

View file

@ -129,18 +129,6 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) } .let { sendEvent(it) }
} }
override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable {
return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty, isUserLocation)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
override fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable {
return localEchoEventFactory.createLiveLocationEvent(beaconInfoEventId, roomId, latitude, longitude, uncertainty)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
override fun redactEvent(event: Event, reason: String?): Cancelable { override fun redactEvent(event: Event, reason: String?): Cancelable {
// TODO manage media/attachements? // TODO manage media/attachements?
val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)

View file

@ -244,7 +244,7 @@ internal class LocalEchoEventFactory @Inject constructor(
) )
} }
fun createLocationEvent( fun createStaticLocationEvent(
roomId: String, roomId: String,
latitude: Double, latitude: Double,
longitude: Double, longitude: Double,

View file

@ -21,33 +21,27 @@ import androidx.lifecycle.LiveData
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStateEventValue import org.matrix.android.sdk.api.query.QueryStateEventValue
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event 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.EventType
import org.matrix.android.sdk.api.session.events.model.toContent 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.GuestAccess import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.api.session.room.state.StateService import org.matrix.android.sdk.api.session.room.state.StateService
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.content.FileUploader import org.matrix.android.sdk.internal.session.content.FileUploader
import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder
internal class DefaultStateService @AssistedInject constructor( internal class DefaultStateService @AssistedInject constructor(
@Assisted private val roomId: String, @Assisted private val roomId: String,
private val stateEventDataSource: StateEventDataSource, private val stateEventDataSource: StateEventDataSource,
private val sendStateTask: SendStateTask, private val sendStateTask: SendStateTask,
private val fileUploader: FileUploader, private val fileUploader: FileUploader,
private val viaParameterFinder: ViaParameterFinder
) : StateService { ) : StateService {
@AssistedFactory @AssistedFactory
@ -191,35 +185,4 @@ internal class DefaultStateService @AssistedInject constructor(
} }
updateJoinRule(RoomJoinRules.RESTRICTED, null, allowEntries) updateJoinRule(RoomJoinRules.RESTRICTED, null, allowEntries)
} }
override suspend fun stopLiveLocation(userId: String) {
getLiveLocationBeaconInfo(userId, true)?.let { beaconInfoStateEvent ->
beaconInfoStateEvent.getClearContent()?.toModel<MessageBeaconInfoContent>()?.let { content ->
val updatedContent = content.copy(isLive = false).toContent()
beaconInfoStateEvent.stateKey?.let {
sendStateEvent(
eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
body = updatedContent,
stateKey = it
)
}
}
}
}
override suspend fun getLiveLocationBeaconInfo(userId: String, filterOnlyLive: Boolean): Event? {
return EventType.STATE_ROOM_BEACON_INFO
.mapNotNull {
stateEventDataSource.getStateEvent(
roomId = roomId,
eventType = it,
stateKey = QueryStringValue.Equals(userId)
)
}
.firstOrNull { beaconInfoEvent ->
!filterOnlyLive ||
beaconInfoEvent.getClearContent()?.toModel<MessageBeaconInfoContent>()?.isLive.orFalse()
}
}
} }

View file

@ -23,10 +23,12 @@ import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.pushers.PusherState import org.matrix.android.sdk.api.session.pushers.PusherState
import org.matrix.android.sdk.internal.database.model.PusherEntity import org.matrix.android.sdk.internal.database.model.PusherEntity
import org.matrix.android.sdk.internal.database.model.PusherEntityFields
import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
import org.matrix.android.sdk.test.fakes.FakeMonarchy import org.matrix.android.sdk.test.fakes.FakeMonarchy
import org.matrix.android.sdk.test.fakes.FakePushersAPI import org.matrix.android.sdk.test.fakes.FakePushersAPI
import org.matrix.android.sdk.test.fakes.FakeRequestExecutor import org.matrix.android.sdk.test.fakes.FakeRequestExecutor
import org.matrix.android.sdk.test.fakes.givenEqualTo
import java.net.SocketException import java.net.SocketException
private val A_JSON_PUSHER = JsonPusher( private val A_JSON_PUSHER = JsonPusher(
@ -56,6 +58,7 @@ class DefaultAddPusherTaskTest {
@Test @Test
fun `given no persisted pusher when adding Pusher then updates api and inserts result with Registered state`() { fun `given no persisted pusher when adding Pusher then updates api and inserts result with Registered state`() {
monarchy.givenWhereReturns<PusherEntity>(result = null) monarchy.givenWhereReturns<PusherEntity>(result = null)
.givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey)
runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
@ -71,6 +74,7 @@ class DefaultAddPusherTaskTest {
fun `given a persisted pusher when adding Pusher then updates api and mutates persisted result with Registered state`() { fun `given a persisted pusher when adding Pusher then updates api and mutates persisted result with Registered state`() {
val realmResult = PusherEntity(appDisplayName = null) val realmResult = PusherEntity(appDisplayName = null)
monarchy.givenWhereReturns(result = realmResult) monarchy.givenWhereReturns(result = realmResult)
.givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey)
runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
@ -84,6 +88,7 @@ class DefaultAddPusherTaskTest {
fun `given a persisted push entity and SetPush API fails when adding Pusher then mutates persisted result with Failed registration state and rethrows`() { fun `given a persisted push entity and SetPush API fails when adding Pusher then mutates persisted result with Failed registration state and rethrows`() {
val realmResult = PusherEntity() val realmResult = PusherEntity()
monarchy.givenWhereReturns(result = realmResult) monarchy.givenWhereReturns(result = realmResult)
.givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey)
pushersAPI.givenSetPusherErrors(SocketException()) pushersAPI.givenSetPusherErrors(SocketException())
assertFailsWith<SocketException> { assertFailsWith<SocketException> {
@ -96,6 +101,7 @@ class DefaultAddPusherTaskTest {
@Test @Test
fun `given no persisted push entity and SetPush API fails when adding Pusher then rethrows error`() { fun `given no persisted push entity and SetPush API fails when adding Pusher then rethrows error`() {
monarchy.givenWhereReturns<PusherEntity>(result = null) monarchy.givenWhereReturns<PusherEntity>(result = null)
.givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey)
pushersAPI.givenSetPusherErrors(SocketException()) pushersAPI.givenSetPusherErrors(SocketException())
assertFailsWith<SocketException> { assertFailsWith<SocketException> {

View file

@ -0,0 +1,171 @@
/*
* 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.location
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.unmockkAll
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.After
import org.junit.Test
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
import org.matrix.android.sdk.test.fakes.FakeMonarchy
import org.matrix.android.sdk.test.fakes.givenEqualTo
import org.matrix.android.sdk.test.fakes.givenIsNotEmpty
import org.matrix.android.sdk.test.fakes.givenIsNotNull
private const val A_ROOM_ID = "room_id"
private const val AN_EVENT_ID = "event_id"
private const val A_LATITUDE = 1.4
private const val A_LONGITUDE = 40.0
private const val AN_UNCERTAINTY = 5.0
private const val A_TIMEOUT = 15_000L
@ExperimentalCoroutinesApi
internal class DefaultLocationSharingServiceTest {
private val fakeRoomId = A_ROOM_ID
private val fakeMonarchy = FakeMonarchy()
private val sendStaticLocationTask = mockk<SendStaticLocationTask>()
private val sendLiveLocationTask = mockk<SendLiveLocationTask>()
private val startLiveLocationShareTask = mockk<StartLiveLocationShareTask>()
private val stopLiveLocationShareTask = mockk<StopLiveLocationShareTask>()
private val fakeLiveLocationShareAggregatedSummaryMapper = mockk<LiveLocationShareAggregatedSummaryMapper>()
private val defaultLocationSharingService = DefaultLocationSharingService(
roomId = fakeRoomId,
monarchy = fakeMonarchy.instance,
sendStaticLocationTask = sendStaticLocationTask,
sendLiveLocationTask = sendLiveLocationTask,
startLiveLocationShareTask = startLiveLocationShareTask,
stopLiveLocationShareTask = stopLiveLocationShareTask,
liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper
)
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `static location can be sent`() = runTest {
val isUserLocation = true
val cancelable = mockk<Cancelable>()
coEvery { sendStaticLocationTask.execute(any()) } returns cancelable
val result = defaultLocationSharingService.sendStaticLocation(
latitude = A_LATITUDE,
longitude = A_LONGITUDE,
uncertainty = AN_UNCERTAINTY,
isUserLocation = isUserLocation
)
result shouldBeEqualTo cancelable
val expectedParams = SendStaticLocationTask.Params(
roomId = A_ROOM_ID,
latitude = A_LATITUDE,
longitude = A_LONGITUDE,
uncertainty = AN_UNCERTAINTY,
isUserLocation = isUserLocation,
)
coVerify { sendStaticLocationTask.execute(expectedParams) }
}
@Test
fun `live location can be sent`() = runTest {
val cancelable = mockk<Cancelable>()
coEvery { sendLiveLocationTask.execute(any()) } returns cancelable
val result = defaultLocationSharingService.sendLiveLocation(
beaconInfoEventId = AN_EVENT_ID,
latitude = A_LATITUDE,
longitude = A_LONGITUDE,
uncertainty = AN_UNCERTAINTY
)
result shouldBeEqualTo cancelable
val expectedParams = SendLiveLocationTask.Params(
roomId = A_ROOM_ID,
beaconInfoEventId = AN_EVENT_ID,
latitude = A_LATITUDE,
longitude = A_LONGITUDE,
uncertainty = AN_UNCERTAINTY
)
coVerify { sendLiveLocationTask.execute(expectedParams) }
}
@Test
fun `live location share can be started with a given timeout`() = runTest {
coEvery { startLiveLocationShareTask.execute(any()) } returns AN_EVENT_ID
val eventId = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
eventId shouldBeEqualTo AN_EVENT_ID
val expectedParams = StartLiveLocationShareTask.Params(
roomId = A_ROOM_ID,
timeoutMillis = A_TIMEOUT
)
coVerify { startLiveLocationShareTask.execute(expectedParams) }
}
@Test
fun `live location share can be stopped`() = runTest {
coEvery { stopLiveLocationShareTask.execute(any()) } just runs
defaultLocationSharingService.stopLiveLocationShare()
val expectedParams = StopLiveLocationShareTask.Params(
roomId = A_ROOM_ID
)
coVerify { stopLiveLocationShareTask.execute(expectedParams) }
}
@Test
fun `livedata of live summaries is correctly computed`() {
val entity = LiveLocationShareAggregatedSummaryEntity()
val summary = LiveLocationShareAggregatedSummary(
userId = "",
isActive = true,
endOfLiveTimestampMillis = 123,
lastLocationDataContent = null
)
fakeMonarchy.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, fakeRoomId)
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
.givenIsNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID)
.givenIsNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT)
fakeMonarchy.givenFindAllMappedWithChangesReturns(
realmEntities = listOf(entity),
mappedResult = listOf(summary),
fakeLiveLocationShareAggregatedSummaryMapper
)
val result = defaultLocationSharingService.getRunningLiveLocationShareSummaries().value
result shouldBeEqualTo listOf(summary)
}
}

View file

@ -0,0 +1,79 @@
/*
* 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.location
import io.mockk.mockk
import io.mockk.unmockkAll
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.After
import org.junit.Test
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor
import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory
private const val A_ROOM_ID = "room_id"
private const val AN_EVENT_ID = "event_id"
private const val A_LATITUDE = 1.4
private const val A_LONGITUDE = 44.0
private const val AN_UNCERTAINTY = 5.0
@ExperimentalCoroutinesApi
internal class DefaultSendLiveLocationTaskTest {
private val fakeLocalEchoEventFactory = FakeLocalEchoEventFactory()
private val fakeEventSenderProcessor = FakeEventSenderProcessor()
private val defaultSendLiveLocationTask = DefaultSendLiveLocationTask(
localEchoEventFactory = fakeLocalEchoEventFactory.instance,
eventSenderProcessor = fakeEventSenderProcessor
)
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given parameters when calling the task then it is correctly executed`() = runTest {
val params = SendLiveLocationTask.Params(
roomId = A_ROOM_ID,
beaconInfoEventId = AN_EVENT_ID,
latitude = A_LATITUDE,
longitude = A_LONGITUDE,
uncertainty = AN_UNCERTAINTY
)
val event = fakeLocalEchoEventFactory.givenCreateLiveLocationEvent(
withLocalEcho = true
)
val cancelable = mockk<Cancelable>()
fakeEventSenderProcessor.givenPostEventReturns(event, cancelable)
val result = defaultSendLiveLocationTask.execute(params)
result shouldBeEqualTo cancelable
fakeLocalEchoEventFactory.verifyCreateLiveLocationEvent(
roomId = params.roomId,
beaconInfoEventId = params.beaconInfoEventId,
latitude = params.latitude,
longitude = params.longitude,
uncertainty = params.uncertainty
)
fakeLocalEchoEventFactory.verifyCreateLocalEcho(event)
}
}

View file

@ -0,0 +1,78 @@
/*
* 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.location
import io.mockk.mockk
import io.mockk.unmockkAll
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.After
import org.junit.Test
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor
import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory
private const val A_ROOM_ID = "room_id"
private const val A_LATITUDE = 1.4
private const val A_LONGITUDE = 44.0
private const val AN_UNCERTAINTY = 5.0
@ExperimentalCoroutinesApi
internal class DefaultSendStaticLocationTaskTest {
private val fakeLocalEchoEventFactory = FakeLocalEchoEventFactory()
private val fakeEventSenderProcessor = FakeEventSenderProcessor()
private val defaultSendStaticLocationTask = DefaultSendStaticLocationTask(
localEchoEventFactory = fakeLocalEchoEventFactory.instance,
eventSenderProcessor = fakeEventSenderProcessor
)
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given parameters when calling the task then it is correctly executed`() = runTest {
val params = SendStaticLocationTask.Params(
roomId = A_ROOM_ID,
latitude = A_LATITUDE,
longitude = A_LONGITUDE,
uncertainty = AN_UNCERTAINTY,
isUserLocation = true
)
val event = fakeLocalEchoEventFactory.givenCreateStaticLocationEvent(
withLocalEcho = true
)
val cancelable = mockk<Cancelable>()
fakeEventSenderProcessor.givenPostEventReturns(event, cancelable)
val result = defaultSendStaticLocationTask.execute(params)
result shouldBeEqualTo cancelable
fakeLocalEchoEventFactory.verifyCreateStaticLocationEvent(
roomId = params.roomId,
latitude = params.latitude,
longitude = params.longitude,
uncertainty = params.uncertainty,
isUserLocation = params.isUserLocation
)
fakeLocalEchoEventFactory.verifyCreateLocalEcho(event)
}
}

View file

@ -0,0 +1,83 @@
/*
* 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.location
import io.mockk.unmockkAll
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.After
import org.junit.Test
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.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
import org.matrix.android.sdk.test.fakes.FakeClock
import org.matrix.android.sdk.test.fakes.FakeSendStateTask
private const val A_USER_ID = "user-id"
private const val A_ROOM_ID = "room-id"
private const val AN_EVENT_ID = "event-id"
private const val A_TIMEOUT = 15_000L
private const val AN_EPOCH = 1655210176L
@ExperimentalCoroutinesApi
internal class DefaultStartLiveLocationShareTaskTest {
private val fakeClock = FakeClock()
private val fakeSendStateTask = FakeSendStateTask()
private val defaultStartLiveLocationShareTask = DefaultStartLiveLocationShareTask(
userId = A_USER_ID,
clock = fakeClock,
sendStateTask = fakeSendStateTask
)
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given parameters when calling the task then it is correctly executed`() = runTest {
val params = StartLiveLocationShareTask.Params(
roomId = A_ROOM_ID,
timeoutMillis = A_TIMEOUT
)
fakeClock.givenEpoch(AN_EPOCH)
fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
val result = defaultStartLiveLocationShareTask.execute(params)
result shouldBeEqualTo AN_EVENT_ID
val expectedBeaconContent = MessageBeaconInfoContent(
timeout = params.timeoutMillis,
isLive = true,
unstableTimestampMillis = AN_EPOCH
).toContent()
val expectedParams = SendStateTask.Params(
roomId = params.roomId,
stateKey = A_USER_ID,
eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
body = expectedBeaconContent
)
fakeSendStateTask.verifyExecuteRetry(
params = expectedParams,
remainingRetry = 3
)
}
}

View file

@ -0,0 +1,92 @@
/*
* 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.location
import io.mockk.unmockkAll
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Test
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.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
import org.matrix.android.sdk.test.fakes.FakeSendStateTask
import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource
private const val A_USER_ID = "user-id"
private const val A_ROOM_ID = "room-id"
private const val AN_EVENT_ID = "event-id"
private const val A_TIMEOUT = 15_000L
private const val AN_EPOCH = 1655210176L
@ExperimentalCoroutinesApi
class DefaultStopLiveLocationShareTaskTest {
private val fakeSendStateTask = FakeSendStateTask()
private val fakeStateEventDataSource = FakeStateEventDataSource()
private val defaultStopLiveLocationShareTask = DefaultStopLiveLocationShareTask(
userId = A_USER_ID,
sendStateTask = fakeSendStateTask,
stateEventDataSource = fakeStateEventDataSource.instance
)
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given parameters when calling the task then it is correctly executed`() = runTest {
val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
val currentStateEvent = Event(
stateKey = A_USER_ID,
content = MessageBeaconInfoContent(
timeout = A_TIMEOUT,
isLive = true,
unstableTimestampMillis = AN_EPOCH
).toContent()
)
fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent)
fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
defaultStopLiveLocationShareTask.execute(params)
val expectedBeaconContent = MessageBeaconInfoContent(
timeout = A_TIMEOUT,
isLive = false,
unstableTimestampMillis = AN_EPOCH
).toContent()
val expectedParams = SendStateTask.Params(
roomId = params.roomId,
stateKey = A_USER_ID,
eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
body = expectedBeaconContent
)
fakeSendStateTask.verifyExecuteRetry(
params = expectedParams,
remainingRetry = 3
)
fakeStateEventDataSource.verifyGetStateEvent(
roomId = params.roomId,
eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
stateKey = A_USER_ID
)
}
}

View file

@ -0,0 +1,30 @@
/*
* 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.every
import io.mockk.mockk
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
internal class FakeEventSenderProcessor : EventSenderProcessor by mockk() {
fun givenPostEventReturns(event: Event, cancelable: Cancelable) {
every { postEvent(event) } returns cancelable
}
}

View file

@ -0,0 +1,106 @@
/*
* 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.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
internal class FakeLocalEchoEventFactory {
val instance = mockk<LocalEchoEventFactory>()
fun givenCreateStaticLocationEvent(withLocalEcho: Boolean): Event {
val event = Event()
every {
instance.createStaticLocationEvent(
roomId = any(),
latitude = any(),
longitude = any(),
uncertainty = any(),
isUserLocation = any()
)
} returns event
if (withLocalEcho) {
every { instance.createLocalEcho(event) } just runs
}
return event
}
fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event {
val event = Event()
every {
instance.createLiveLocationEvent(
beaconInfoEventId = any(),
roomId = any(),
latitude = any(),
longitude = any(),
uncertainty = any()
)
} returns event
if (withLocalEcho) {
every { instance.createLocalEcho(event) } just runs
}
return event
}
fun verifyCreateStaticLocationEvent(
roomId: String,
latitude: Double,
longitude: Double,
uncertainty: Double?,
isUserLocation: Boolean
) {
verify {
instance.createStaticLocationEvent(
roomId = roomId,
latitude = latitude,
longitude = longitude,
uncertainty = uncertainty,
isUserLocation = isUserLocation
)
}
}
fun verifyCreateLiveLocationEvent(
roomId: String,
beaconInfoEventId: String,
latitude: Double,
longitude: Double,
uncertainty: Double?
) {
verify {
instance.createLiveLocationEvent(
roomId = roomId,
beaconInfoEventId = beaconInfoEventId,
latitude = latitude,
longitude = longitude,
uncertainty = uncertainty
)
}
}
fun verifyCreateLocalEcho(event: Event) {
verify { instance.createLocalEcho(event) }
}
}

View file

@ -16,40 +16,62 @@
package org.matrix.android.sdk.test.fakes package org.matrix.android.sdk.test.fakes
import androidx.lifecycle.MutableLiveData
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.mockk.MockKVerificationScope import io.mockk.MockKVerificationScope
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.verify import io.mockk.slot
import io.realm.Realm import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
internal class FakeMonarchy { internal class FakeMonarchy {
val instance = mockk<Monarchy>() val instance = mockk<Monarchy>()
private val realm = mockk<Realm>(relaxed = true) private val fakeRealm = FakeRealm()
init { init {
mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt") mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt")
coEvery { coEvery {
instance.awaitTransaction(any<suspend (Realm) -> Any>()) instance.awaitTransaction(any<suspend (Realm) -> Any>())
} coAnswers { } coAnswers {
secondArg<suspend (Realm) -> Any>().invoke(realm) secondArg<suspend (Realm) -> Any>().invoke(fakeRealm.instance)
} }
} }
inline fun <reified T : RealmModel> givenWhereReturns(result: T?) { inline fun <reified T : RealmModel> givenWhere(): RealmQuery<T> {
val queryResult = mockk<RealmQuery<T>>(relaxed = true) return fakeRealm.givenWhere()
every { queryResult.findFirst() } returns result }
every { realm.where<T>() } returns queryResult
inline fun <reified T : RealmModel> givenWhereReturns(result: T?): RealmQuery<T> {
return fakeRealm.givenWhere<T>()
.givenFindFirst(result)
} }
inline fun <reified T : RealmModel> verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) { inline fun <reified T : RealmModel> verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) {
verify { realm.insertOrUpdate(verification()) } fakeRealm.verifyInsertOrUpdate(verification)
}
inline fun <reified R, reified T : RealmModel> givenFindAllMappedWithChangesReturns(
realmEntities: List<T>,
mappedResult: List<R>,
mapper: Monarchy.Mapper<R, T>
) {
every { mapper.map(any()) } returns mockk()
val monarchyQuery = slot<Monarchy.Query<T>>()
val monarchyMapper = slot<Monarchy.Mapper<R, T>>()
every {
instance.findAllMappedWithChanges(capture(monarchyQuery), capture(monarchyMapper))
} answers {
monarchyQuery.captured.createQuery(fakeRealm.instance)
realmEntities.forEach {
monarchyMapper.captured.map(it)
}
MutableLiveData(mappedResult)
}
} }
} }

View file

@ -16,8 +16,10 @@
package org.matrix.android.sdk.test.fakes package org.matrix.android.sdk.test.fakes
import io.mockk.MockKVerificationScope
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify
import io.realm.Realm import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import io.realm.RealmQuery import io.realm.RealmQuery
@ -33,6 +35,10 @@ internal class FakeRealm {
every { instance.where<T>() } returns query every { instance.where<T>() } returns query
return query return query
} }
inline fun <reified T : RealmModel> verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) {
verify { instance.insertOrUpdate(verification()) }
}
} }
inline fun <reified T : RealmModel> RealmQuery<T>.givenFindFirst( inline fun <reified T : RealmModel> RealmQuery<T>.givenFindFirst(
@ -77,3 +83,17 @@ inline fun <reified T : RealmModel> RealmQuery<T>.givenNotEqualTo(
every { notEqualTo(fieldName, value) } returns this every { notEqualTo(fieldName, value) } returns this
return this return this
} }
inline fun <reified T : RealmModel> RealmQuery<T>.givenIsNotEmpty(
fieldName: String
): RealmQuery<T> {
every { isNotEmpty(fieldName) } returns this
return this
}
inline fun <reified T : RealmModel> RealmQuery<T>.givenIsNotNull(
fieldName: String
): RealmQuery<T> {
every { isNotNull(fieldName) } returns this
return this
}

View file

@ -0,0 +1,33 @@
/*
* 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.state.SendStateTask
internal class FakeSendStateTask : SendStateTask by mockk() {
fun givenExecuteRetryReturns(eventId: String) {
coEvery { executeRetry(any(), any()) } returns eventId
}
fun verifyExecuteRetry(params: SendStateTask.Params, remainingRetry: Int) {
coVerify { executeRetry(params, remainingRetry) }
}
}

View file

@ -0,0 +1,49 @@
/*
* 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.every
import io.mockk.mockk
import io.mockk.verify
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
internal class FakeStateEventDataSource {
val instance: StateEventDataSource = mockk()
fun givenGetStateEventReturns(event: Event) {
every {
instance.getStateEvent(
roomId = any(),
eventType = any(),
stateKey = any()
)
} returns event
}
fun verifyGetStateEvent(roomId: String, eventType: String, stateKey: String) {
verify {
instance.getStateEvent(
roomId = roomId,
eventType = eventType,
stateKey = QueryStringValue.Equals(stateKey)
)
}
}
}

View file

@ -23,16 +23,13 @@ import android.os.Parcelable
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.services.VectorService import im.vector.app.core.services.VectorService
import im.vector.app.core.time.Clock
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
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.getRoom import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import timber.log.Timber import timber.log.Timber
import java.util.Timer import java.util.Timer
import java.util.TimerTask import java.util.TimerTask
@ -51,7 +48,6 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
@Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var notificationUtils: NotificationUtils
@Inject lateinit var locationTracker: LocationTracker @Inject lateinit var locationTracker: LocationTracker
@Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var clock: Clock
private val binder = LocalBinder() private val binder = LocalBinder()
@ -84,34 +80,19 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
scheduleTimer(roomArgs.roomId, roomArgs.durationMillis) scheduleTimer(roomArgs.roomId, roomArgs.durationMillis)
// Send beacon info state event // Send beacon info state event
activeSessionHolder launchInIO { session ->
.getSafeActiveSession() sendStartingLiveBeaconInfo(session, roomArgs)
?.let { session -> }
session.coroutineScope.launch(session.coroutineDispatchers.io) {
sendStartingLiveBeaconInfo(session, roomArgs)
}
}
} }
return START_STICKY return START_STICKY
} }
private suspend fun sendStartingLiveBeaconInfo(session: Session, roomArgs: RoomArgs) { private suspend fun sendStartingLiveBeaconInfo(session: Session, roomArgs: RoomArgs) {
val beaconContent = MessageBeaconInfoContent(
timeout = roomArgs.durationMillis,
isLive = true,
unstableTimestampMillis = clock.epochMillis()
).toContent()
val stateKey = session.myUserId
val beaconEventId = session val beaconEventId = session
.getRoom(roomArgs.roomId) .getRoom(roomArgs.roomId)
?.stateService() ?.locationSharingService()
?.sendStateEvent( ?.startLiveLocationShare(timeoutMillis = roomArgs.durationMillis)
eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
stateKey = stateKey,
body = beaconContent
)
beaconEventId beaconEventId
?.takeUnless { it.isEmpty() } ?.takeUnless { it.isEmpty() }
@ -159,13 +140,11 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
} }
private fun sendStoppedBeaconInfo(roomId: String) { private fun sendStoppedBeaconInfo(roomId: String) {
activeSessionHolder launchInIO { session ->
.getSafeActiveSession() session.getRoom(roomId)
?.let { session -> ?.locationSharingService()
session.coroutineScope.launch(session.coroutineDispatchers.io) { ?.stopLiveLocationShare()
session.getRoom(roomId)?.stateService()?.stopLiveLocation(session.myUserId) }
}
}
} }
override fun onLocationUpdate(locationData: LocationData) { override fun onLocationUpdate(locationData: LocationData) {
@ -182,20 +161,16 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
beaconInfoEventId: String, beaconInfoEventId: String,
locationData: LocationData locationData: LocationData
) { ) {
val session = activeSessionHolder.getSafeActiveSession() launchInIO { session ->
val room = session?.getRoom(roomId) session.getRoom(roomId)
val userId = session?.myUserId ?.locationSharingService()
?.sendLiveLocation(
if (room == null || userId == null) { beaconInfoEventId = beaconInfoEventId,
return latitude = locationData.latitude,
longitude = locationData.longitude,
uncertainty = locationData.uncertainty
)
} }
room.sendService().sendLiveLocation(
beaconInfoEventId = beaconInfoEventId,
latitude = locationData.latitude,
longitude = locationData.longitude,
uncertainty = locationData.uncertainty
)
} }
override fun onNoLocationProviderAvailable() { override fun onNoLocationProviderAvailable() {
@ -216,6 +191,16 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
destroyMe() destroyMe()
} }
private fun launchInIO(block: suspend CoroutineScope.(Session) -> Unit) =
activeSessionHolder
.getSafeActiveSession()
?.let { session ->
session.coroutineScope.launch(
context = session.coroutineDispatchers.io,
block = { block(session) }
)
}
override fun onBind(intent: Intent?): IBinder { override fun onBind(intent: Intent?): IBinder {
return binder return binder
} }

View file

@ -136,13 +136,15 @@ class LocationSharingViewModel @AssistedInject constructor(
private fun shareLocation(locationData: LocationData?, isUserLocation: Boolean) { private fun shareLocation(locationData: LocationData?, isUserLocation: Boolean) {
locationData?.let { location -> locationData?.let { location ->
room.sendService().sendLocation( viewModelScope.launch {
latitude = location.latitude, room.locationSharingService().sendStaticLocation(
longitude = location.longitude, latitude = location.latitude,
uncertainty = location.uncertainty, longitude = location.longitude,
isUserLocation = isUserLocation uncertainty = location.uncertainty,
) isUserLocation = isUserLocation
_viewEvents.post(LocationSharingViewEvents.Close) )
_viewEvents.post(LocationSharingViewEvents.Close)
}
} ?: run { } ?: run {
_viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError) _viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError)
} }