Merge pull request #6356 from vector-im/fix/mna/stop-lls-from-other-device

[Location sharing] - Make stop of a live from another device possible (PSF-1060)
This commit is contained in:
Maxime NATUREL 2022-06-29 09:45:44 +02:00 committed by GitHub
commit d112f860a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 649 additions and 232 deletions

1
changelog.d/6349.bugfix Normal file
View File

@ -0,0 +1 @@
[Location sharing] Fix stop of a live not possible from another device

View File

@ -16,9 +16,11 @@
package org.matrix.android.sdk.api.session.room.location package org.matrix.android.sdk.api.session.room.location
import androidx.annotation.MainThread
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 import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
/** /**
* Manage all location sharing related features. * Manage all location sharing related features.
@ -59,5 +61,13 @@ interface LocationSharingService {
/** /**
* Returns a LiveData on the list of current running live location shares. * Returns a LiveData on the list of current running live location shares.
*/ */
@MainThread
fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>> fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>>
/**
* Returns a LiveData on the live location share summary with the given eventId.
* @param beaconInfoEventId event id of the initial beacon info state event
*/
@MainThread
fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData<Optional<LiveLocationShareAggregatedSummary>>
} }

View File

@ -76,7 +76,7 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveIn
realm: Realm, realm: Realm,
roomId: String, roomId: String,
userId: String, userId: String,
ignoredEventId: String ignoredEventId: String,
): List<LiveLocationShareAggregatedSummaryEntity> { ): List<LiveLocationShareAggregatedSummaryEntity> {
return LiveLocationShareAggregatedSummaryEntity return LiveLocationShareAggregatedSummaryEntity
.whereRoomId(realm, roomId = roomId) .whereRoomId(realm, roomId = roomId)

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.room.location package org.matrix.android.sdk.internal.session.room.location
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@ -25,9 +26,12 @@ import org.matrix.android.sdk.api.session.room.location.LocationSharingService
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
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.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
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.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
internal class DefaultLocationSharingService @AssistedInject constructor( internal class DefaultLocationSharingService @AssistedInject constructor(
@ -88,4 +92,15 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
liveLocationShareAggregatedSummaryMapper liveLocationShareAggregatedSummaryMapper
) )
} }
override fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData<Optional<LiveLocationShareAggregatedSummary>> {
return Transformations.map(
monarchy.findAllMappedWithChanges(
{ LiveLocationShareAggregatedSummaryEntity.where(it, roomId = roomId, eventId = beaconInfoEventId) },
liveLocationShareAggregatedSummaryMapper
)
) {
it.firstOrNull().toOptional()
}
}
} }

View File

@ -16,18 +16,27 @@
package org.matrix.android.sdk.internal.session.room.location package org.matrix.android.sdk.internal.session.room.location
import androidx.arch.core.util.Function
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkAll import io.mockk.unmockkAll
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.After import org.junit.After
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
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.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
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.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
@ -46,7 +55,6 @@ private const val A_TIMEOUT = 15_000L
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
internal class DefaultLocationSharingServiceTest { internal class DefaultLocationSharingServiceTest {
private val fakeRoomId = A_ROOM_ID
private val fakeMonarchy = FakeMonarchy() private val fakeMonarchy = FakeMonarchy()
private val sendStaticLocationTask = mockk<SendStaticLocationTask>() private val sendStaticLocationTask = mockk<SendStaticLocationTask>()
private val sendLiveLocationTask = mockk<SendLiveLocationTask>() private val sendLiveLocationTask = mockk<SendLiveLocationTask>()
@ -55,7 +63,7 @@ internal class DefaultLocationSharingServiceTest {
private val fakeLiveLocationShareAggregatedSummaryMapper = mockk<LiveLocationShareAggregatedSummaryMapper>() private val fakeLiveLocationShareAggregatedSummaryMapper = mockk<LiveLocationShareAggregatedSummaryMapper>()
private val defaultLocationSharingService = DefaultLocationSharingService( private val defaultLocationSharingService = DefaultLocationSharingService(
roomId = fakeRoomId, roomId = A_ROOM_ID,
monarchy = fakeMonarchy.instance, monarchy = fakeMonarchy.instance,
sendStaticLocationTask = sendStaticLocationTask, sendStaticLocationTask = sendStaticLocationTask,
sendLiveLocationTask = sendLiveLocationTask, sendLiveLocationTask = sendLiveLocationTask,
@ -64,6 +72,11 @@ internal class DefaultLocationSharingServiceTest {
liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper
) )
@Before
fun setUp() {
mockkStatic("androidx.lifecycle.Transformations")
}
@After @After
fun tearDown() { fun tearDown() {
unmockkAll() unmockkAll()
@ -154,7 +167,7 @@ internal class DefaultLocationSharingServiceTest {
) )
fakeMonarchy.givenWhere<LiveLocationShareAggregatedSummaryEntity>() fakeMonarchy.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, fakeRoomId) .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID)
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
.givenIsNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID) .givenIsNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID)
.givenIsNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT) .givenIsNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT)
@ -168,4 +181,38 @@ internal class DefaultLocationSharingServiceTest {
result shouldBeEqualTo listOf(summary) result shouldBeEqualTo listOf(summary)
} }
@Test
fun `given an event id when getting livedata on corresponding live summary then it is correctly computed`() {
val entity = LiveLocationShareAggregatedSummaryEntity()
val summary = LiveLocationShareAggregatedSummary(
userId = "",
isActive = true,
endOfLiveTimestampMillis = 123,
lastLocationDataContent = null
)
fakeMonarchy.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID)
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID)
val liveData = fakeMonarchy.givenFindAllMappedWithChangesReturns(
realmEntities = listOf(entity),
mappedResult = listOf(summary),
fakeLiveLocationShareAggregatedSummaryMapper
)
val mapper = slot<Function<List<LiveLocationShareAggregatedSummary>, Optional<LiveLocationShareAggregatedSummary>>>()
every {
Transformations.map(
liveData,
capture(mapper)
)
} answers {
val value = secondArg<Function<List<LiveLocationShareAggregatedSummary>, Optional<LiveLocationShareAggregatedSummary>>>().apply(listOf(summary))
MutableLiveData(value)
}
val result = defaultLocationSharingService.getLiveLocationShareSummary(AN_EVENT_ID).value
result shouldBeEqualTo summary.toOptional()
}
} }

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.test.fakes package org.matrix.android.sdk.test.fakes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.mockk.MockKVerificationScope import io.mockk.MockKVerificationScope
@ -60,10 +61,11 @@ internal class FakeMonarchy {
realmEntities: List<T>, realmEntities: List<T>,
mappedResult: List<R>, mappedResult: List<R>,
mapper: Monarchy.Mapper<R, T> mapper: Monarchy.Mapper<R, T>
) { ): LiveData<List<R>> {
every { mapper.map(any()) } returns mockk() every { mapper.map(any()) } returns mockk()
val monarchyQuery = slot<Monarchy.Query<T>>() val monarchyQuery = slot<Monarchy.Query<T>>()
val monarchyMapper = slot<Monarchy.Mapper<R, T>>() val monarchyMapper = slot<Monarchy.Mapper<R, T>>()
val result = MutableLiveData(mappedResult)
every { every {
instance.findAllMappedWithChanges(capture(monarchyQuery), capture(monarchyMapper)) instance.findAllMappedWithChanges(capture(monarchyQuery), capture(monarchyMapper))
} answers { } answers {
@ -71,7 +73,8 @@ internal class FakeMonarchy {
realmEntities.forEach { realmEntities.forEach {
monarchyMapper.captured.map(it) monarchyMapper.captured.map(it)
} }
MutableLiveData(mappedResult) result
} }
return result
} }
} }

View File

@ -44,11 +44,11 @@ class ActiveSessionHolder @Inject constructor(
private val guardServiceStarter: GuardServiceStarter private val guardServiceStarter: GuardServiceStarter
) { ) {
private var activeSession: AtomicReference<Session?> = AtomicReference() private var activeSessionReference: AtomicReference<Session?> = AtomicReference()
fun setActiveSession(session: Session) { fun setActiveSession(session: Session) {
Timber.w("setActiveSession of ${session.myUserId}") Timber.w("setActiveSession of ${session.myUserId}")
activeSession.set(session) activeSessionReference.set(session)
activeSessionDataSource.post(Option.just(session)) activeSessionDataSource.post(Option.just(session))
keyRequestHandler.start(session) keyRequestHandler.start(session)
@ -68,7 +68,7 @@ class ActiveSessionHolder @Inject constructor(
it.removeListener(sessionListener) it.removeListener(sessionListener)
} }
activeSession.set(null) activeSessionReference.set(null)
activeSessionDataSource.post(Option.empty()) activeSessionDataSource.post(Option.empty())
keyRequestHandler.stop() keyRequestHandler.stop()
@ -80,15 +80,15 @@ class ActiveSessionHolder @Inject constructor(
} }
fun hasActiveSession(): Boolean { fun hasActiveSession(): Boolean {
return activeSession.get() != null return activeSessionReference.get() != null
} }
fun getSafeActiveSession(): Session? { fun getSafeActiveSession(): Session? {
return activeSession.get() return activeSessionReference.get()
} }
fun getActiveSession(): Session { fun getActiveSession(): Session {
return activeSession.get() return activeSessionReference.get()
?: throw IllegalStateException("You should authenticate before using this") ?: throw IllegalStateException("You should authenticate before using this")
} }

View File

@ -53,6 +53,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.location.LocationSharingServiceConnection import im.vector.app.features.location.LocationSharingServiceConnection
import im.vector.app.features.location.live.StopLiveLocationShareUseCase
import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.raw.wellknown.getOutboundSessionKeySharingStrategyOrDefault import im.vector.app.features.raw.wellknown.getOutboundSessionKeySharingStrategyOrDefault
@ -92,6 +93,7 @@ import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
@ -133,8 +135,9 @@ class TimelineViewModel @AssistedInject constructor(
private val decryptionFailureTracker: DecryptionFailureTracker, private val decryptionFailureTracker: DecryptionFailureTracker,
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
private val locationSharingServiceConnection: LocationSharingServiceConnection, private val locationSharingServiceConnection: LocationSharingServiceConnection,
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
timelineFactory: TimelineFactory, timelineFactory: TimelineFactory,
appStateHandler: AppStateHandler appStateHandler: AppStateHandler,
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback { Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback {
@ -1139,7 +1142,12 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleStopLiveLocationSharing() { private fun handleStopLiveLocationSharing() {
locationSharingServiceConnection.stopLiveLocationSharing(room.roomId) viewModelScope.launch {
val result = stopLiveLocationShareUseCase.execute(room.roomId)
if (result is UpdateLiveLocationShareResult.Failure) {
_viewEvents.post(RoomDetailViewEvents.Failure(throwable = result.error, showInDialog = true))
}
}
} }
private fun observeRoomSummary() { private fun observeRoomSummary() {
@ -1310,7 +1318,7 @@ class TimelineViewModel @AssistedInject constructor(
// we should also mark it as read here, for the scenario that the user // we should also mark it as read here, for the scenario that the user
// is already in the thread timeline // is already in the thread timeline
markThreadTimelineAsReadLocal() markThreadTimelineAsReadLocal()
locationSharingServiceConnection.unbind() locationSharingServiceConnection.unbind(this)
super.onCleared() super.onCleared()
} }
} }

View File

@ -23,17 +23,21 @@ 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.features.location.live.GetLiveLocationShareSummaryUseCase
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.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
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.getRoom import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
import timber.log.Timber import timber.log.Timber
import java.util.Timer
import java.util.TimerTask
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -49,6 +53,7 @@ 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 getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase
private val binder = LocalBinder() private val binder = LocalBinder()
@ -56,16 +61,27 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
* Keep track of a map between beacon event Id starting the live and RoomArgs. * Keep track of a map between beacon event Id starting the live and RoomArgs.
*/ */
private val roomArgsMap = mutableMapOf<String, RoomArgs>() private val roomArgsMap = mutableMapOf<String, RoomArgs>()
private val timers = mutableListOf<Timer>()
var callback: Callback? = null var callback: Callback? = null
private val jobs = mutableListOf<Job>()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Timber.i("### LocationSharingService.onCreate") Timber.i("### LocationSharingService.onCreate")
initLocationTracking()
}
private fun initLocationTracking() {
// Start tracking location // Start tracking location
locationTracker.addCallback(this) locationTracker.addCallback(this)
locationTracker.start() locationTracker.start()
launchWithActiveSession { session ->
val job = locationTracker.locations
.onEach(this@LocationSharingService::onLocationUpdate)
.launchIn(session.coroutineScope)
jobs.add(job)
}
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -78,11 +94,8 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
val notification = notificationUtils.buildLiveLocationSharingNotification() val notification = notificationUtils.buildLiveLocationSharingNotification()
startForeground(roomArgs.roomId.hashCode(), notification) startForeground(roomArgs.roomId.hashCode(), notification)
// Schedule a timer to stop sharing
scheduleTimer(roomArgs.roomId, roomArgs.durationMillis)
// Send beacon info state event // Send beacon info state event
launchInIO { session -> launchWithActiveSession { session ->
sendStartingLiveBeaconInfo(session, roomArgs) sendStartingLiveBeaconInfo(session, roomArgs)
} }
} }
@ -100,7 +113,8 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
?.let { result -> ?.let { result ->
when (result) { when (result) {
is UpdateLiveLocationShareResult.Success -> { is UpdateLiveLocationShareResult.Success -> {
roomArgsMap[result.beaconEventId] = roomArgs addRoomArgs(result.beaconEventId, roomArgs)
listenForLiveSummaryChanges(roomArgs.roomId, result.beaconEventId)
locationTracker.requestLastKnownLocation() locationTracker.requestLastKnownLocation()
} }
is UpdateLiveLocationShareResult.Failure -> { is UpdateLiveLocationShareResult.Failure -> {
@ -115,49 +129,13 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
} }
} }
private fun scheduleTimer(roomId: String, durationMillis: Long) { private fun stopSharingLocation(roomId: String) {
Timer()
.apply {
schedule(object : TimerTask() {
override fun run() {
stopSharingLocation(roomId)
timers.remove(this@apply)
}
}, durationMillis)
}
.also {
timers.add(it)
}
}
fun stopSharingLocation(roomId: String) {
Timber.i("### LocationSharingService.stopSharingLocation for $roomId") Timber.i("### LocationSharingService.stopSharingLocation for $roomId")
removeRoomArgs(roomId)
launchInIO { session -> tryToDestroyMe()
when (val result = sendStoppedBeaconInfo(session, roomId)) {
is UpdateLiveLocationShareResult.Success -> {
synchronized(roomArgsMap) {
val beaconIds = roomArgsMap
.filter { it.value.roomId == roomId }
.map { it.key }
beaconIds.forEach { roomArgsMap.remove(it) }
tryToDestroyMe()
}
}
is UpdateLiveLocationShareResult.Failure -> callback?.onServiceError(result.error)
else -> Unit
}
}
} }
private suspend fun sendStoppedBeaconInfo(session: Session, roomId: String): UpdateLiveLocationShareResult? { private fun onLocationUpdate(locationData: LocationData) {
return session.getRoom(roomId)
?.locationSharingService()
?.stopLiveLocationShare()
}
override fun onLocationUpdate(locationData: LocationData) {
Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}") Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}")
// Emit location update to all rooms in which live location sharing is active // Emit location update to all rooms in which live location sharing is active
@ -171,7 +149,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
beaconInfoEventId: String, beaconInfoEventId: String,
locationData: LocationData locationData: LocationData
) { ) {
launchInIO { session -> launchWithActiveSession { session ->
session.getRoom(roomId) session.getRoom(roomId)
?.locationSharingService() ?.locationSharingService()
?.sendLiveLocation( ?.sendLiveLocation(
@ -191,29 +169,44 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
private fun tryToDestroyMe() { private fun tryToDestroyMe() {
if (roomArgsMap.isEmpty()) { if (roomArgsMap.isEmpty()) {
Timber.i("### LocationSharingService. Destroying self, time is up for all rooms") Timber.i("### LocationSharingService. Destroying self, time is up for all rooms")
destroyMe() stopSelf()
} }
} }
private fun destroyMe() {
locationTracker.removeCallback(this)
timers.forEach { it.cancel() }
timers.clear()
stopSelf()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
Timber.i("### LocationSharingService.onDestroy") Timber.i("### LocationSharingService.onDestroy")
destroyMe() jobs.forEach { it.cancel() }
jobs.clear()
locationTracker.removeCallback(this)
} }
private fun launchInIO(block: suspend CoroutineScope.(Session) -> Unit) = private fun addRoomArgs(beaconEventId: String, roomArgs: RoomArgs) {
roomArgsMap[beaconEventId] = roomArgs
}
private fun removeRoomArgs(roomId: String) {
roomArgsMap.toMap()
.filter { it.value.roomId == roomId }
.forEach { roomArgsMap.remove(it.key) }
}
private fun listenForLiveSummaryChanges(roomId: String, eventId: String) {
launchWithActiveSession { session ->
val job = getLiveLocationShareSummaryUseCase.execute(roomId, eventId)
.distinctUntilChangedBy { it.isActive }
.filter { it.isActive == false }
.onEach { stopSharingLocation(roomId) }
.launchIn(session.coroutineScope)
jobs.add(job)
}
}
private fun launchWithActiveSession(block: suspend CoroutineScope.(Session) -> Unit) =
activeSessionHolder activeSessionHolder
.getSafeActiveSession() .getSafeActiveSession()
?.let { session -> ?.let { session ->
session.coroutineScope.launch( session.coroutineScope.launch(
context = session.coroutineDispatchers.io,
block = { block(session) } block = { block(session) }
) )
} }

View File

@ -22,7 +22,9 @@ import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.IBinder import android.os.IBinder
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LocationSharingServiceConnection @Inject constructor( class LocationSharingServiceConnection @Inject constructor(
private val context: Context private val context: Context
) : ServiceConnection, LocationSharingService.Callback { ) : ServiceConnection, LocationSharingService.Callback {
@ -33,12 +35,12 @@ class LocationSharingServiceConnection @Inject constructor(
fun onLocationServiceError(error: Throwable) fun onLocationServiceError(error: Throwable)
} }
private var callback: Callback? = null private val callbacks = mutableSetOf<Callback>()
private var isBound = false private var isBound = false
private var locationSharingService: LocationSharingService? = null private var locationSharingService: LocationSharingService? = null
fun bind(callback: Callback) { fun bind(callback: Callback) {
this.callback = callback addCallback(callback)
if (isBound) { if (isBound) {
callback.onLocationServiceRunning() callback.onLocationServiceRunning()
@ -49,12 +51,8 @@ class LocationSharingServiceConnection @Inject constructor(
} }
} }
fun unbind() { fun unbind(callback: Callback) {
callback = null removeCallback(callback)
}
fun stopLiveLocationSharing(roomId: String) {
locationSharingService?.stopSharingLocation(roomId)
} }
override fun onServiceConnected(className: ComponentName, binder: IBinder) { override fun onServiceConnected(className: ComponentName, binder: IBinder) {
@ -62,17 +60,33 @@ class LocationSharingServiceConnection @Inject constructor(
it.callback = this it.callback = this
} }
isBound = true isBound = true
callback?.onLocationServiceRunning() onCallbackActionNoArg(Callback::onLocationServiceRunning)
} }
override fun onServiceDisconnected(className: ComponentName) { override fun onServiceDisconnected(className: ComponentName) {
isBound = false isBound = false
locationSharingService?.callback = null locationSharingService?.callback = null
locationSharingService = null locationSharingService = null
callback?.onLocationServiceStopped() onCallbackActionNoArg(Callback::onLocationServiceStopped)
} }
override fun onServiceError(error: Throwable) { override fun onServiceError(error: Throwable) {
callback?.onLocationServiceError(error) forwardErrorToCallbacks(error)
}
private fun addCallback(callback: Callback) {
callbacks.add(callback)
}
private fun removeCallback(callback: Callback) {
callbacks.remove(callback)
}
private fun onCallbackActionNoArg(action: Callback.() -> Unit) {
callbacks.toList().forEach(action)
}
private fun forwardErrorToCallbacks(error: Throwable) {
callbacks.toList().forEach { it.onLocationServiceError(error) }
} }
} }

View File

@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getUser import org.matrix.android.sdk.api.session.getUser
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber
/** /**
* Sampling period to compare target location and user location. * Sampling period to compare target location and user location.
@ -65,13 +66,20 @@ class LocationSharingViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<LocationSharingViewModel, LocationSharingViewState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<LocationSharingViewModel, LocationSharingViewState> by hiltMavericksViewModelFactory()
init { init {
locationTracker.addCallback(this) initLocationTracking()
locationTracker.start()
setUserItem() setUserItem()
updatePin() updatePin()
compareTargetAndUserLocation() compareTargetAndUserLocation()
} }
private fun initLocationTracking() {
locationTracker.addCallback(this)
locationTracker.locations
.onEach(::onLocationUpdate)
.launchIn(viewModelScope)
locationTracker.start()
}
private fun setUserItem() { private fun setUserItem() {
setState { copy(userItem = session.getUser(session.myUserId)?.toMatrixItem()) } setState { copy(userItem = session.getUser(session.myUserId)?.toMatrixItem()) }
} }
@ -172,7 +180,8 @@ class LocationSharingViewModel @AssistedInject constructor(
) )
} }
override fun onLocationUpdate(locationData: LocationData) { private fun onLocationUpdate(locationData: LocationData) {
Timber.d("onLocationUpdate()")
setState { setState {
copy(lastKnownUserLocation = locationData) copy(lastKnownUserLocation = locationData)
} }

View File

@ -25,28 +25,27 @@ import androidx.annotation.VisibleForTesting
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.location.LocationListenerCompat import androidx.core.location.LocationListenerCompat
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.core.utils.Debouncer import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.utils.createBackgroundHandler import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
private const val BKG_HANDLER_NAME = "LocationTracker.BKG_HANDLER_NAME"
private const val LOCATION_DEBOUNCE_ID = "LocationTracker.LOCATION_DEBOUNCE_ID"
@Singleton @Singleton
class LocationTracker @Inject constructor( class LocationTracker @Inject constructor(
context: Context context: Context,
private val activeSessionHolder: ActiveSessionHolder
) : LocationListenerCompat { ) : LocationListenerCompat {
private val locationManager = context.getSystemService<LocationManager>() private val locationManager = context.getSystemService<LocationManager>()
interface Callback { interface Callback {
/**
* Called on every location update.
*/
fun onLocationUpdate(locationData: LocationData)
/** /**
* Called when no location provider is available to request location updates. * Called when no location provider is available to request location updates.
*/ */
@ -62,9 +61,16 @@ class LocationTracker @Inject constructor(
@VisibleForTesting @VisibleForTesting
var hasLocationFromGPSProvider = false var hasLocationFromGPSProvider = false
private var lastLocation: LocationData? = null private val _locations = MutableSharedFlow<Location>(replay = 1)
private val debouncer = Debouncer(createBackgroundHandler(BKG_HANDLER_NAME)) /**
* SharedFlow to collect location updates.
*/
val locations = _locations.asSharedFlow()
.onEach { Timber.d("new location emitted") }
.debounce(MIN_TIME_TO_UPDATE_LOCATION_MILLIS)
.onEach { Timber.d("new location emitted after debounce") }
.map { it.toLocationData() }
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
fun start() { fun start() {
@ -119,33 +125,35 @@ class LocationTracker @Inject constructor(
} }
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
@VisibleForTesting
fun stop() { fun stop() {
Timber.d("stop()") Timber.d("stop()")
locationManager?.removeUpdates(this) locationManager?.removeUpdates(this)
synchronized(this) { callbacks.clear()
callbacks.clear()
}
debouncer.cancelAll()
hasLocationFromGPSProvider = false hasLocationFromGPSProvider = false
hasLocationFromFusedProvider = false hasLocationFromFusedProvider = false
} }
/** /**
* Request the last known location. It will be given async through Callback. * Request the last known location. It will be given async through corresponding flow.
* Please ensure adding a callback to receive the value. * Please ensure collecting the flow before calling this method.
*/ */
fun requestLastKnownLocation() { fun requestLastKnownLocation() {
lastLocation?.let { locationData -> onLocationUpdate(locationData) } Timber.d("requestLastKnownLocation")
activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch {
_locations.replayCache.firstOrNull()?.let {
Timber.d("emitting last location from cache")
_locations.emit(it)
}
}
} }
@Synchronized
fun addCallback(callback: Callback) { fun addCallback(callback: Callback) {
if (!callbacks.contains(callback)) { if (!callbacks.contains(callback)) {
callbacks.add(callback) callbacks.add(callback)
} }
} }
@Synchronized
fun removeCallback(callback: Callback) { fun removeCallback(callback: Callback) {
callbacks.remove(callback) callbacks.remove(callback)
if (callbacks.size == 0) { if (callbacks.size == 0) {
@ -183,21 +191,19 @@ class LocationTracker @Inject constructor(
} }
} }
debouncer.debounce(LOCATION_DEBOUNCE_ID, MIN_TIME_TO_UPDATE_LOCATION_MILLIS) { notifyLocation(location)
notifyLocation(location)
}
} }
private fun notifyLocation(location: Location) { private fun notifyLocation(location: Location) {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch {
Timber.d("notify location: $location") if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
} else { Timber.d("notify location: $location")
Timber.d("notify location: ${location.provider}") } else {
} Timber.d("notify location: ${location.provider}")
}
val locationData = location.toLocationData() _locations.emit(location)
lastLocation = locationData }
onLocationUpdate(locationData)
} }
override fun onProviderDisabled(provider: String) { override fun onProviderDisabled(provider: String) {
@ -215,9 +221,8 @@ class LocationTracker @Inject constructor(
} }
} }
@Synchronized
private fun onNoLocationProviderAvailable() { private fun onNoLocationProviderAvailable() {
callbacks.forEach { callbacks.toList().forEach {
try { try {
it.onNoLocationProviderAvailable() it.onNoLocationProviderAvailable()
} catch (error: Exception) { } catch (error: Exception) {
@ -226,17 +231,6 @@ class LocationTracker @Inject constructor(
} }
} }
@Synchronized
private fun onLocationUpdate(locationData: LocationData) {
callbacks.forEach {
try {
it.onLocationUpdate(locationData)
} catch (error: Exception) {
Timber.e(error, "error in onLocationUpdate callback $it")
}
}
}
private fun Location.toLocationData(): LocationData { private fun Location.toLocationData(): LocationData {
return LocationData(latitude, longitude, accuracy.toDouble()) return LocationData(latitude, longitude, accuracy.toDouble())
} }

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.location.live
import androidx.lifecycle.asFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
import timber.log.Timber
import javax.inject.Inject
class GetLiveLocationShareSummaryUseCase @Inject constructor(
private val session: Session,
) {
suspend fun execute(roomId: String, eventId: String): Flow<LiveLocationShareAggregatedSummary> = withContext(session.coroutineDispatchers.main) {
Timber.d("getting flow for roomId=$roomId and eventId=$eventId")
session.getRoom(roomId)
?.locationSharingService()
?.getLiveLocationShareSummary(eventId)
?.asFlow()
?.mapNotNull { it.getOrNull() }
?: emptyFlow()
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.location.live
import im.vector.app.core.di.ActiveSessionHolder
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
import javax.inject.Inject
class StopLiveLocationShareUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder
) {
suspend fun execute(roomId: String): UpdateLiveLocationShareResult? {
return sendStoppedBeaconInfo(roomId)
}
private suspend fun sendStoppedBeaconInfo(roomId: String): UpdateLiveLocationShareResult? {
return activeSessionHolder.getActiveSession()
.getRoom(roomId)
?.locationSharingService()
?.stopLiveLocationShare()
}
}

View File

@ -24,13 +24,17 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.location.LocationSharingServiceConnection import im.vector.app.features.location.LocationSharingServiceConnection
import im.vector.app.features.location.live.StopLiveLocationShareUseCase
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
class LocationLiveMapViewModel @AssistedInject constructor( class LocationLiveMapViewModel @AssistedInject constructor(
@Assisted private val initialState: LocationLiveMapViewState, @Assisted private val initialState: LocationLiveMapViewState,
getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase, getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase,
private val locationSharingServiceConnection: LocationSharingServiceConnection, private val locationSharingServiceConnection: LocationSharingServiceConnection,
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
) : VectorViewModel<LocationLiveMapViewState, LocationLiveMapAction, LocationLiveMapViewEvents>(initialState), LocationSharingServiceConnection.Callback { ) : VectorViewModel<LocationLiveMapViewState, LocationLiveMapAction, LocationLiveMapViewEvents>(initialState), LocationSharingServiceConnection.Callback {
@AssistedFactory @AssistedFactory
@ -47,6 +51,11 @@ class LocationLiveMapViewModel @AssistedInject constructor(
locationSharingServiceConnection.bind(this) locationSharingServiceConnection.bind(this)
} }
override fun onCleared() {
locationSharingServiceConnection.unbind(this)
super.onCleared()
}
override fun handle(action: LocationLiveMapAction) { override fun handle(action: LocationLiveMapAction) {
when (action) { when (action) {
is LocationLiveMapAction.AddMapSymbol -> handleAddMapSymbol(action) is LocationLiveMapAction.AddMapSymbol -> handleAddMapSymbol(action)
@ -70,7 +79,12 @@ class LocationLiveMapViewModel @AssistedInject constructor(
} }
private fun handleStopSharing() { private fun handleStopSharing() {
locationSharingServiceConnection.stopLiveLocationSharing(initialState.roomId) viewModelScope.launch {
val result = stopLiveLocationShareUseCase.execute(initialState.roomId)
if (result is UpdateLiveLocationShareResult.Failure) {
_viewEvents.post(LocationLiveMapViewEvents.Error(result.error))
}
}
} }
override fun onLocationServiceRunning() { override fun onLocationServiceRunning() {

View File

@ -19,21 +19,21 @@ package im.vector.app.features.location
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import im.vector.app.core.utils.Debouncer import im.vector.app.features.session.coroutineScope
import im.vector.app.core.utils.createBackgroundHandler import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakeHandler
import im.vector.app.test.fakes.FakeLocationManager import im.vector.app.test.fakes.FakeLocationManager
import im.vector.app.test.test
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkConstructor
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.runs import io.mockk.runs
import io.mockk.slot
import io.mockk.unmockkAll import io.mockk.unmockkAll
import io.mockk.verify import io.mockk.verify
import io.mockk.verifyOrder import io.mockk.verifyOrder
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -45,26 +45,18 @@ private const val AN_ACCURACY = 5.0f
class LocationTrackerTest { class LocationTrackerTest {
private val fakeHandler = FakeHandler()
private val fakeLocationManager = FakeLocationManager() private val fakeLocationManager = FakeLocationManager()
private val fakeContext = FakeContext().also { private val fakeContext = FakeContext().also {
it.givenService(Context.LOCATION_SERVICE, android.location.LocationManager::class.java, fakeLocationManager.instance) it.givenService(Context.LOCATION_SERVICE, android.location.LocationManager::class.java, fakeLocationManager.instance)
} }
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private lateinit var locationTracker: LocationTracker private lateinit var locationTracker: LocationTracker
@Before @Before
fun setUp() { fun setUp() {
mockkConstructor(Debouncer::class) mockkStatic("im.vector.app.features.session.SessionCoroutineScopesKt")
every { anyConstructed<Debouncer>().cancelAll() } just runs locationTracker = LocationTracker(fakeContext.instance, fakeActiveSessionHolder.instance)
val runnable = slot<Runnable>()
every { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, capture(runnable)) } answers {
runnable.captured.run()
true
}
mockkStatic("im.vector.app.core.utils.HandlerKt")
every { createBackgroundHandler(any()) } returns fakeHandler.instance
locationTracker = LocationTracker(fakeContext.instance)
fakeLocationManager.givenRemoveUpdates(locationTracker) fakeLocationManager.givenRemoveUpdates(locationTracker)
} }
@ -139,13 +131,11 @@ class LocationTrackerTest {
} }
@Test @Test
fun `when location updates are received from fused provider then fused locations are taken in priority`() { fun `when location updates are received from fused provider then fused locations are taken in priority`() = runTest {
every { fakeActiveSessionHolder.fakeSession.coroutineScope } returns this
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER) val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER)
mockAvailableProviders(providers) mockAvailableProviders(providers)
val callback = mockCallback()
locationTracker.addCallback(callback)
locationTracker.start() locationTracker.start()
val fusedLocation = mockLocation( val fusedLocation = mockLocation(
provider = LocationManager.FUSED_PROVIDER, provider = LocationManager.FUSED_PROVIDER,
latitude = 1.0, latitude = 1.0,
@ -159,29 +149,31 @@ class LocationTrackerTest {
val networkLocation = mockLocation( val networkLocation = mockLocation(
provider = LocationManager.NETWORK_PROVIDER provider = LocationManager.NETWORK_PROVIDER
) )
val resultUpdates = locationTracker.locations.test(this)
locationTracker.onLocationChanged(fusedLocation) locationTracker.onLocationChanged(fusedLocation)
locationTracker.onLocationChanged(gpsLocation) locationTracker.onLocationChanged(gpsLocation)
locationTracker.onLocationChanged(networkLocation) locationTracker.onLocationChanged(networkLocation)
advanceTimeBy(MIN_TIME_TO_UPDATE_LOCATION_MILLIS + 1)
val expectedLocationData = LocationData( val expectedLocationData = LocationData(
latitude = 1.0, latitude = 1.0,
longitude = 3.0, longitude = 3.0,
uncertainty = 4.0 uncertainty = 4.0
) )
verify { callback.onLocationUpdate(expectedLocationData) } resultUpdates
verify { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) } .assertValues(listOf(expectedLocationData))
.finish()
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo true locationTracker.hasLocationFromFusedProvider shouldBeEqualTo true
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false
} }
@Test @Test
fun `when location updates are received from gps provider then gps locations are taken if none are received from fused provider`() { fun `when location updates are received from gps provider then gps locations are taken if none are received from fused provider`() = runTest {
every { fakeActiveSessionHolder.fakeSession.coroutineScope } returns this
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER) val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER)
mockAvailableProviders(providers) mockAvailableProviders(providers)
val callback = mockCallback()
locationTracker.addCallback(callback)
locationTracker.start() locationTracker.start()
val gpsLocation = mockLocation( val gpsLocation = mockLocation(
provider = LocationManager.GPS_PROVIDER, provider = LocationManager.GPS_PROVIDER,
latitude = 1.0, latitude = 1.0,
@ -192,66 +184,75 @@ class LocationTrackerTest {
val networkLocation = mockLocation( val networkLocation = mockLocation(
provider = LocationManager.NETWORK_PROVIDER provider = LocationManager.NETWORK_PROVIDER
) )
val resultUpdates = locationTracker.locations.test(this)
locationTracker.onLocationChanged(gpsLocation) locationTracker.onLocationChanged(gpsLocation)
locationTracker.onLocationChanged(networkLocation) locationTracker.onLocationChanged(networkLocation)
advanceTimeBy(MIN_TIME_TO_UPDATE_LOCATION_MILLIS + 1)
val expectedLocationData = LocationData( val expectedLocationData = LocationData(
latitude = 1.0, latitude = 1.0,
longitude = 3.0, longitude = 3.0,
uncertainty = 4.0 uncertainty = 4.0
) )
verify { callback.onLocationUpdate(expectedLocationData) } resultUpdates
verify { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) } .assertValues(listOf(expectedLocationData))
.finish()
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo true locationTracker.hasLocationFromGPSProvider shouldBeEqualTo true
} }
@Test @Test
fun `when location updates are received from network provider then network locations are taken if none are received from fused or gps provider`() { fun `when location updates are received from network provider then network locations are taken if none are received from fused, gps provider`() = runTest {
every { fakeActiveSessionHolder.fakeSession.coroutineScope } returns this
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER) val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER)
mockAvailableProviders(providers) mockAvailableProviders(providers)
val callback = mockCallback()
locationTracker.addCallback(callback)
locationTracker.start() locationTracker.start()
val networkLocation = mockLocation( val networkLocation = mockLocation(
provider = LocationManager.NETWORK_PROVIDER, provider = LocationManager.NETWORK_PROVIDER,
latitude = 1.0, latitude = 1.0,
longitude = 3.0, longitude = 3.0,
accuracy = 4f accuracy = 4f
) )
val resultUpdates = locationTracker.locations.test(this)
locationTracker.onLocationChanged(networkLocation) locationTracker.onLocationChanged(networkLocation)
advanceTimeBy(MIN_TIME_TO_UPDATE_LOCATION_MILLIS + 1)
val expectedLocationData = LocationData( val expectedLocationData = LocationData(
latitude = 1.0, latitude = 1.0,
longitude = 3.0, longitude = 3.0,
uncertainty = 4.0 uncertainty = 4.0
) )
verify { callback.onLocationUpdate(expectedLocationData) } resultUpdates
verify { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) } .assertValues(listOf(expectedLocationData))
.finish()
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false
} }
@Test @Test
fun `when requesting the last location then last location is notified via callback`() { fun `when requesting the last location then last location is notified via location updates flow`() = runTest {
every { fakeActiveSessionHolder.fakeSession.coroutineScope } returns this
val providers = listOf(LocationManager.GPS_PROVIDER) val providers = listOf(LocationManager.GPS_PROVIDER)
fakeLocationManager.givenActiveProviders(providers) fakeLocationManager.givenActiveProviders(providers)
val lastLocation = mockLocation(provider = LocationManager.GPS_PROVIDER) val lastLocation = mockLocation(provider = LocationManager.GPS_PROVIDER)
fakeLocationManager.givenLastLocationForProvider(provider = LocationManager.GPS_PROVIDER, location = lastLocation) fakeLocationManager.givenLastLocationForProvider(provider = LocationManager.GPS_PROVIDER, location = lastLocation)
fakeLocationManager.givenRequestUpdatesForProvider(provider = LocationManager.GPS_PROVIDER, listener = locationTracker) fakeLocationManager.givenRequestUpdatesForProvider(provider = LocationManager.GPS_PROVIDER, listener = locationTracker)
val callback = mockCallback()
locationTracker.addCallback(callback)
locationTracker.start() locationTracker.start()
val resultUpdates = locationTracker.locations.test(this)
locationTracker.requestLastKnownLocation() locationTracker.requestLastKnownLocation()
advanceTimeBy(MIN_TIME_TO_UPDATE_LOCATION_MILLIS + 1)
val expectedLocationData = LocationData( val expectedLocationData = LocationData(
latitude = A_LATITUDE, latitude = A_LATITUDE,
longitude = A_LONGITUDE, longitude = A_LONGITUDE,
uncertainty = AN_ACCURACY.toDouble() uncertainty = AN_ACCURACY.toDouble()
) )
verify { callback.onLocationUpdate(expectedLocationData) } resultUpdates
.assertValues(listOf(expectedLocationData))
.finish()
} }
@Test @Test
@ -259,7 +260,6 @@ class LocationTrackerTest {
locationTracker.stop() locationTracker.stop()
verify { fakeLocationManager.instance.removeUpdates(locationTracker) } verify { fakeLocationManager.instance.removeUpdates(locationTracker) }
verify { anyConstructed<Debouncer>().cancelAll() }
locationTracker.callbacks.isEmpty() shouldBeEqualTo true locationTracker.callbacks.isEmpty() shouldBeEqualTo true
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false
@ -276,7 +276,6 @@ class LocationTrackerTest {
private fun mockCallback(): LocationTracker.Callback { private fun mockCallback(): LocationTracker.Callback {
return mockk<LocationTracker.Callback>().also { return mockk<LocationTracker.Callback>().also {
every { it.onNoLocationProviderAvailable() } just runs every { it.onNoLocationProviderAvailable() } just runs
every { it.onLocationUpdate(any()) } just runs
} }
} }

View File

@ -16,21 +16,16 @@
package im.vector.app.features.location.domain.usecase package im.vector.app.features.location.domain.usecase
import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationData
import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeSession
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.OverrideMockKs import io.mockk.impl.annotations.OverrideMockKs
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
class CompareLocationsUseCaseTest { class CompareLocationsUseCaseTest {
@get:Rule
val mvRxTestRule = MvRxTestRule()
private val session = FakeSession() private val session = FakeSession()
@OverrideMockKs @OverrideMockKs

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.location.live
import im.vector.app.test.fakes.FakeFlowLiveDataConversions
import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.givenAsFlowReturns
import io.mockk.unmockkAll
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.After
import org.junit.Before
import org.junit.Test
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.util.Optional
private const val A_ROOM_ID = "room_id"
private const val AN_EVENT_ID = "event_id"
class GetLiveLocationShareSummaryUseCaseTest {
private val fakeSession = FakeSession()
private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
private val getLiveLocationShareSummaryUseCase = GetLiveLocationShareSummaryUseCase(
session = fakeSession
)
@Before
fun setUp() {
fakeFlowLiveDataConversions.setup()
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a room id and event id when calling use case then live data on summary is returned`() = runTest {
val summary = LiveLocationShareAggregatedSummary(
userId = "userId",
isActive = true,
endOfLiveTimestampMillis = 123,
lastLocationDataContent = MessageBeaconLocationDataContent()
)
fakeSession.roomService()
.getRoom(A_ROOM_ID)
.locationSharingService()
.givenLiveLocationShareSummaryReturns(AN_EVENT_ID, summary)
.givenAsFlowReturns(Optional(summary))
val result = getLiveLocationShareSummaryUseCase.execute(A_ROOM_ID, AN_EVENT_ID).first()
result shouldBeEqualTo summary
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.location.live
import im.vector.app.test.fakes.FakeActiveSessionHolder
import io.mockk.unmockkAll
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.location.UpdateLiveLocationShareResult
private const val A_ROOM_ID = "room_id"
private const val AN_EVENT_ID = "event_id"
class StopLiveLocationShareUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val stopLiveLocationShareUseCase = StopLiveLocationShareUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance
)
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a room id when calling use case then the current live is stopped with success`() = runTest {
val updateLiveResult = UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
fakeActiveSessionHolder
.fakeSession
.roomService()
.getRoom(A_ROOM_ID)
.locationSharingService()
.givenStopLiveLocationShareReturns(updateLiveResult)
val result = stopLiveLocationShareUseCase.execute(A_ROOM_ID)
result shouldBeEqualTo updateLiveResult
}
@Test
fun `given a room id and error during the process when calling use case then result is failure`() = runTest {
val error = Throwable()
val updateLiveResult = UpdateLiveLocationShareResult.Failure(error)
fakeActiveSessionHolder
.fakeSession
.roomService()
.getRoom(A_ROOM_ID)
.locationSharingService()
.givenStopLiveLocationShareReturns(updateLiveResult)
val result = stopLiveLocationShareUseCase.execute(A_ROOM_ID)
result shouldBeEqualTo updateLiveResult
}
}

View File

@ -16,52 +16,48 @@
package im.vector.app.features.location.live.map package im.vector.app.features.location.live.map
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationData
import im.vector.app.test.fakes.FakeFlowLiveDataConversions
import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.givenAsFlowReturns
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.unmockkAll
import io.mockk.unmockkStatic
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.shouldBeEqualTo
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
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.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
private const val A_ROOM_ID = "room_id"
class GetListOfUserLiveLocationUseCaseTest { class GetListOfUserLiveLocationUseCaseTest {
@get:Rule
val mvRxTestRule = MvRxTestRule()
private val fakeSession = FakeSession() private val fakeSession = FakeSession()
private val viewStateMapper = mockk<UserLiveLocationViewStateMapper>() private val viewStateMapper = mockk<UserLiveLocationViewStateMapper>()
private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
private val getListOfUserLiveLocationUseCase = GetListOfUserLiveLocationUseCase(fakeSession, viewStateMapper) private val getListOfUserLiveLocationUseCase = GetListOfUserLiveLocationUseCase(
session = fakeSession,
userLiveLocationViewStateMapper = viewStateMapper
)
@Before @Before
fun setUp() { fun setUp() {
mockkStatic("androidx.lifecycle.FlowLiveDataConversions") fakeFlowLiveDataConversions.setup()
} }
@After @After
fun tearDown() { fun tearDown() {
unmockkStatic("androidx.lifecycle.FlowLiveDataConversions") unmockkAll()
} }
@Test @Test
fun `given a room id then the correct flow of view states list is collected`() = runTest { fun `given a room id then the correct flow of view states list is collected`() = runTest {
val roomId = "roomId"
val summary1 = LiveLocationShareAggregatedSummary( val summary1 = LiveLocationShareAggregatedSummary(
userId = "userId1", userId = "userId1",
isActive = true, isActive = true,
@ -81,12 +77,11 @@ class GetListOfUserLiveLocationUseCaseTest {
lastLocationDataContent = MessageBeaconLocationDataContent() lastLocationDataContent = MessageBeaconLocationDataContent()
) )
val summaries = listOf(summary1, summary2, summary3) val summaries = listOf(summary1, summary2, summary3)
val liveData = fakeSession.roomService() fakeSession.roomService()
.getRoom(roomId) .getRoom(A_ROOM_ID)
.locationSharingService() .locationSharingService()
.givenRunningLiveLocationShareSummaries(summaries) .givenRunningLiveLocationShareSummariesReturns(summaries)
.givenAsFlowReturns(summaries)
every { liveData.asFlow() } returns flowOf(summaries)
val viewState1 = UserLiveLocationViewState( val viewState1 = UserLiveLocationViewState(
matrixItem = MatrixItem.UserItem(id = "@userId1:matrix.org", displayName = "User 1", avatarUrl = ""), matrixItem = MatrixItem.UserItem(id = "@userId1:matrix.org", displayName = "User 1", avatarUrl = ""),
@ -108,8 +103,8 @@ class GetListOfUserLiveLocationUseCaseTest {
coEvery { viewStateMapper.map(summary2) } returns viewState2 coEvery { viewStateMapper.map(summary2) } returns viewState2
coEvery { viewStateMapper.map(summary3) } returns null coEvery { viewStateMapper.map(summary3) } returns null
val viewStates = getListOfUserLiveLocationUseCase.execute(roomId).first() val viewStates = getListOfUserLiveLocationUseCase.execute(A_ROOM_ID).first()
assertEquals(listOf(viewState1, viewState2), viewStates) viewStates shouldBeEqualTo listOf(viewState1, viewState2)
} }
} }

View File

@ -18,39 +18,47 @@ package im.vector.app.features.location.live.map
import com.airbnb.mvrx.test.MvRxTestRule import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationData
import im.vector.app.features.location.LocationSharingServiceConnection import im.vector.app.features.location.live.StopLiveLocationShareUseCase
import im.vector.app.test.fakes.FakeLocationSharingServiceConnection
import im.vector.app.test.test import im.vector.app.test.test
import io.mockk.every import io.mockk.every
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
private const val A_ROOM_ID = "room_id"
class LocationLiveMapViewModelTest { class LocationLiveMapViewModelTest {
@get:Rule @get:Rule
val mvrxTestRule = MvRxTestRule() val mvRxTestRule = MvRxTestRule(testDispatcher = UnconfinedTestDispatcher())
private val fakeRoomId = "" private val args = LocationLiveMapViewArgs(roomId = A_ROOM_ID)
private val args = LocationLiveMapViewArgs(roomId = fakeRoomId)
private val getListOfUserLiveLocationUseCase = mockk<GetListOfUserLiveLocationUseCase>() private val getListOfUserLiveLocationUseCase = mockk<GetListOfUserLiveLocationUseCase>()
private val locationServiceConnection = mockk<LocationSharingServiceConnection>() private val locationServiceConnection = FakeLocationSharingServiceConnection()
private val stopLiveLocationShareUseCase = mockk<StopLiveLocationShareUseCase>()
private fun createViewModel(): LocationLiveMapViewModel { private fun createViewModel(): LocationLiveMapViewModel {
return LocationLiveMapViewModel( return LocationLiveMapViewModel(
LocationLiveMapViewState(args), LocationLiveMapViewState(args),
getListOfUserLiveLocationUseCase, getListOfUserLiveLocationUseCase,
locationServiceConnection locationServiceConnection.instance,
stopLiveLocationShareUseCase
) )
} }
@After
fun tearDown() {
unmockkAll()
}
@Test @Test
fun `given the viewModel has been initialized then viewState contains user locations list`() = runTest { fun `given the viewModel has been initialized then viewState contains user locations list`() = runTest {
val userLocations = listOf( val userLocations = listOf(
@ -63,8 +71,8 @@ class LocationLiveMapViewModelTest {
showStopSharingButton = false showStopSharingButton = false
) )
) )
every { locationServiceConnection.bind(any()) } just runs locationServiceConnection.givenBind()
every { getListOfUserLiveLocationUseCase.execute(fakeRoomId) } returns flowOf(userLocations) every { getListOfUserLiveLocationUseCase.execute(A_ROOM_ID) } returns flowOf(userLocations)
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel viewModel
@ -76,6 +84,6 @@ class LocationLiveMapViewModelTest {
) )
.finish() .finish()
verify { locationServiceConnection.bind(viewModel) } locationServiceConnection.verifyBind(viewModel)
} }
} }

View File

@ -19,7 +19,6 @@ package im.vector.app.features.media.domain.usecase
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.intent.getMimeTypeFromUri
import im.vector.app.core.utils.saveMedia import im.vector.app.core.utils.saveMedia
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
@ -42,14 +41,10 @@ import io.mockk.verifyAll
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
class DownloadMediaUseCaseTest { class DownloadMediaUseCaseTest {
@get:Rule
val mvRxTestRule = MvRxTestRule()
@MockK @MockK
lateinit var appContext: Context lateinit var appContext: Context

View File

@ -16,13 +16,15 @@
package im.vector.app.test package im.vector.app.test
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
private val testDispatcher = UnconfinedTestDispatcher()
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers( internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(
io = Dispatchers.Main, io = testDispatcher,
computation = Dispatchers.Main, computation = testDispatcher,
main = Dispatchers.Main, main = testDispatcher,
crypto = Dispatchers.Main, crypto = testDispatcher,
dmVerif = Dispatchers.Main dmVerif = testDispatcher
) )

View File

@ -23,10 +23,11 @@ import io.mockk.mockk
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
class FakeActiveSessionHolder( class FakeActiveSessionHolder(
private val fakeSession: FakeSession = FakeSession() val fakeSession: FakeSession = FakeSession()
) { ) {
val instance = mockk<ActiveSessionHolder> { val instance = mockk<ActiveSessionHolder> {
every { getActiveSession() } returns fakeSession every { getActiveSession() } returns fakeSession
every { getSafeActiveSession() } returns fakeSession
} }
fun expectSetsActiveSession(session: Session) { fun expectSetsActiveSession(session: Session) {

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow
import io.mockk.every
import io.mockk.mockkStatic
import kotlinx.coroutines.flow.flowOf
class FakeFlowLiveDataConversions {
fun setup() {
mockkStatic("androidx.lifecycle.FlowLiveDataConversions")
}
}
fun <T> LiveData<T>.givenAsFlowReturns(value: T) {
every { asFlow() } returns flowOf(value)
}

View File

@ -18,17 +18,34 @@ package im.vector.app.test.fakes
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
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.location.UpdateLiveLocationShareResult
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.Optional
class FakeLocationSharingService : LocationSharingService by mockk() { class FakeLocationSharingService : LocationSharingService by mockk() {
fun givenRunningLiveLocationShareSummaries(summaries: List<LiveLocationShareAggregatedSummary>): fun givenRunningLiveLocationShareSummariesReturns(
LiveData<List<LiveLocationShareAggregatedSummary>> { summaries: List<LiveLocationShareAggregatedSummary>
): LiveData<List<LiveLocationShareAggregatedSummary>> {
return MutableLiveData(summaries).also { return MutableLiveData(summaries).also {
every { getRunningLiveLocationShareSummaries() } returns it every { getRunningLiveLocationShareSummaries() } returns it
} }
} }
fun givenLiveLocationShareSummaryReturns(
eventId: String,
summary: LiveLocationShareAggregatedSummary
): LiveData<Optional<LiveLocationShareAggregatedSummary>> {
return MutableLiveData(Optional(summary)).also {
every { getLiveLocationShareSummary(eventId) } returns it
}
}
fun givenStopLiveLocationShareReturns(result: UpdateLiveLocationShareResult) {
coEvery { stopLiveLocationShare() } returns result
}
} }

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import im.vector.app.features.location.LocationSharingServiceConnection
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
class FakeLocationSharingServiceConnection {
val instance = mockk<LocationSharingServiceConnection>()
fun givenBind() {
every { instance.bind(any()) } just runs
}
fun verifyBind(callback: LocationSharingServiceConnection.Callback) {
verify { instance.bind(callback) }
}
}