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:
commit
d112f860a2
1
changelog.d/6349.bugfix
Normal file
1
changelog.d/6349.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Location sharing] Fix stop of a live not possible from another device
|
@ -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>>
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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() {
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user