Adding use case to stop live location share

WIP
This commit is contained in:
Maxime NATUREL 2022-06-16 12:04:37 +02:00
parent cd72563d8b
commit 8406b2a4eb
8 changed files with 204 additions and 33 deletions

View File

@ -53,6 +53,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.typing.TypingHelper
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.powerlevel.PowerLevelsFlowFactory
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.room.getStateEvent
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.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
@ -133,8 +135,9 @@ class TimelineViewModel @AssistedInject constructor(
private val decryptionFailureTracker: DecryptionFailureTracker,
private val notificationDrawerManager: NotificationDrawerManager,
private val locationSharingServiceConnection: LocationSharingServiceConnection,
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
timelineFactory: TimelineFactory,
appStateHandler: AppStateHandler
appStateHandler: AppStateHandler,
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback {
@ -1139,7 +1142,12 @@ class TimelineViewModel @AssistedInject constructor(
}
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() {
@ -1310,7 +1318,7 @@ class TimelineViewModel @AssistedInject constructor(
// we should also mark it as read here, for the scenario that the user
// is already in the thread timeline
markThreadTimelineAsReadLocal()
locationSharingServiceConnection.unbind()
locationSharingServiceConnection.unbind(this)
super.onCleared()
}
}

View File

@ -132,31 +132,16 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
fun stopSharingLocation(roomId: String) {
Timber.i("### LocationSharingService.stopSharingLocation for $roomId")
synchronized(roomArgsMap) {
val beaconIds = roomArgsMap
.filter { it.value.roomId == roomId }
.map { it.key }
beaconIds.forEach { roomArgsMap.remove(it) }
launchInIO { session ->
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
}
tryToDestroyMe()
}
}
private suspend fun sendStoppedBeaconInfo(session: Session, roomId: String): UpdateLiveLocationShareResult? {
return session.getRoom(roomId)
?.locationSharingService()
?.stopLiveLocationShare()
}
override fun onLocationUpdate(locationData: LocationData) {
Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}")

View File

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

View File

@ -0,0 +1,44 @@
/*
* 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.features.location.LocationSharingServiceConnection
import org.matrix.android.sdk.api.session.Session
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 locationSharingServiceConnection: LocationSharingServiceConnection,
private val session: Session
) {
suspend fun execute(roomId: String): UpdateLiveLocationShareResult? {
val result = sendStoppedBeaconInfo(session, roomId)
when (result) {
is UpdateLiveLocationShareResult.Success -> locationSharingServiceConnection.stopLiveLocationSharing(roomId)
else -> Unit
}
return result
}
private suspend fun sendStoppedBeaconInfo(session: Session, roomId: String): UpdateLiveLocationShareResult? {
return session.getRoom(roomId)
?.locationSharingService()
?.stopLiveLocationShare()
}
}

View File

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

View File

@ -0,0 +1,60 @@
/*
* 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.FakeLocationSharingServiceConnection
import im.vector.app.test.fakes.FakeSession
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 fakeLocationSharingServiceConnection = FakeLocationSharingServiceConnection()
private val fakeSession = FakeSession()
private val stopLiveLocationShareUseCase = StopLiveLocationShareUseCase(
locationSharingServiceConnection = fakeLocationSharingServiceConnection.instance,
session = fakeSession
)
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a room id when calling use case then the current live is stopped with success`() = runTest {
fakeLocationSharingServiceConnection.givenStopLiveLocationSharing()
val result = stopLiveLocationShareUseCase.execute(A_ROOM_ID)
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
fakeLocationSharingServiceConnection.verifyStopLiveLocationSharing(A_ROOM_ID)
}
@Test
fun `given a room id and error during the process when calling use case then result is failure`() = runTest {
}
}

View File

@ -24,6 +24,7 @@ import io.mockk.coEvery
import io.mockk.every
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.flowOf
@ -55,7 +56,7 @@ class GetListOfUserLiveLocationUseCaseTest {
@After
fun tearDown() {
unmockkStatic("androidx.lifecycle.FlowLiveDataConversions")
unmockkAll()
}
@Test

View File

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