Merge pull request #6316 from vector-im/fix/mna/crash-offline-lls

[Location sharing] Fix crash when starting/stopping a live when offline (PSF-1124)
This commit is contained in:
Maxime NATUREL 2022-06-20 14:00:56 +02:00 committed by GitHub
commit 32c6281dd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 268 additions and 54 deletions

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

@ -0,0 +1 @@
[Location sharing] Fix crash when starting/stopping a live when offline

View File

@ -46,14 +46,15 @@ interface LocationSharingService {
/** /**
* Starts sharing live location in the room. * Starts sharing live location in the room.
* @param timeoutMillis timeout of the live in milliseconds * @param timeoutMillis timeout of the live in milliseconds
* @return the id of the created beacon info event * @return the result of the update of the live
*/ */
suspend fun startLiveLocationShare(timeoutMillis: Long): String suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult
/** /**
* Stops sharing live location in the room. * Stops sharing live location in the room.
* @return the result of the update of the live
*/ */
suspend fun stopLiveLocationShare() suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult
/** /**
* Returns a LiveData on the list of current running live location shares. * Returns a LiveData on the list of current running live location shares.

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.location
/**
* Represents the result of an update of live location share like a start or a stop.
*/
sealed interface UpdateLiveLocationShareResult {
data class Success(val beaconEventId: String) : UpdateLiveLocationShareResult
data class Failure(val error: Throwable) : UpdateLiveLocationShareResult
}

View File

@ -22,6 +22,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import org.matrix.android.sdk.api.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.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper
@ -66,7 +67,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
return sendLiveLocationTask.execute(params) return sendLiveLocationTask.execute(params)
} }
override suspend fun startLiveLocationShare(timeoutMillis: Long): String { override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult {
val params = StartLiveLocationShareTask.Params( val params = StartLiveLocationShareTask.Params(
roomId = roomId, roomId = roomId,
timeoutMillis = timeoutMillis timeoutMillis = timeoutMillis
@ -74,7 +75,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
return startLiveLocationShareTask.execute(params) return startLiveLocationShareTask.execute(params)
} }
override suspend fun stopLiveLocationShare() { override suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult {
val params = StopLiveLocationShareTask.Params( val params = StopLiveLocationShareTask.Params(
roomId = roomId, roomId = roomId,
) )

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.location
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.internal.session.room.state.SendStateTask
@ -25,7 +26,7 @@ import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import javax.inject.Inject import javax.inject.Inject
internal interface StartLiveLocationShareTask : Task<StartLiveLocationShareTask.Params, String> { internal interface StartLiveLocationShareTask : Task<StartLiveLocationShareTask.Params, UpdateLiveLocationShareResult> {
data class Params( data class Params(
val roomId: String, val roomId: String,
val timeoutMillis: Long, val timeoutMillis: Long,
@ -38,7 +39,7 @@ internal class DefaultStartLiveLocationShareTask @Inject constructor(
private val sendStateTask: SendStateTask, private val sendStateTask: SendStateTask,
) : StartLiveLocationShareTask { ) : StartLiveLocationShareTask {
override suspend fun execute(params: StartLiveLocationShareTask.Params): String { override suspend fun execute(params: StartLiveLocationShareTask.Params): UpdateLiveLocationShareResult {
val beaconContent = MessageBeaconInfoContent( val beaconContent = MessageBeaconInfoContent(
timeout = params.timeoutMillis, timeout = params.timeoutMillis,
isLive = true, isLive = true,
@ -51,6 +52,15 @@ internal class DefaultStartLiveLocationShareTask @Inject constructor(
eventType = eventType, eventType = eventType,
body = beaconContent body = beaconContent
) )
return sendStateTask.executeRetry(sendStateTaskParams, 3) return try {
val eventId = sendStateTask.executeRetry(sendStateTaskParams, 3)
if (eventId.isNotEmpty()) {
UpdateLiveLocationShareResult.Success(eventId)
} else {
UpdateLiveLocationShareResult.Failure(Exception("empty event id for new state event"))
}
} catch (error: Throwable) {
UpdateLiveLocationShareResult.Failure(error)
}
} }
} }

View File

@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.internal.session.room.state.SendStateTask
@ -29,7 +30,7 @@ import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject import javax.inject.Inject
internal interface StopLiveLocationShareTask : Task<StopLiveLocationShareTask.Params, Unit> { internal interface StopLiveLocationShareTask : Task<StopLiveLocationShareTask.Params, UpdateLiveLocationShareResult> {
data class Params( data class Params(
val roomId: String, val roomId: String,
) )
@ -41,10 +42,10 @@ internal class DefaultStopLiveLocationShareTask @Inject constructor(
private val stateEventDataSource: StateEventDataSource, private val stateEventDataSource: StateEventDataSource,
) : StopLiveLocationShareTask { ) : StopLiveLocationShareTask {
override suspend fun execute(params: StopLiveLocationShareTask.Params) { override suspend fun execute(params: StopLiveLocationShareTask.Params): UpdateLiveLocationShareResult {
val beaconInfoStateEvent = getLiveLocationBeaconInfoForUser(userId, params.roomId) ?: return val beaconInfoStateEvent = getLiveLocationBeaconInfoForUser(userId, params.roomId) ?: return getResultForIncorrectBeaconInfoEvent()
val stateKey = beaconInfoStateEvent.stateKey ?: return val stateKey = beaconInfoStateEvent.stateKey ?: return getResultForIncorrectBeaconInfoEvent()
val content = beaconInfoStateEvent.getClearContent()?.toModel<MessageBeaconInfoContent>() ?: return val content = beaconInfoStateEvent.getClearContent()?.toModel<MessageBeaconInfoContent>() ?: return getResultForIncorrectBeaconInfoEvent()
val updatedContent = content.copy(isLive = false).toContent() val updatedContent = content.copy(isLive = false).toContent()
val sendStateTaskParams = SendStateTask.Params( val sendStateTaskParams = SendStateTask.Params(
roomId = params.roomId, roomId = params.roomId,
@ -52,9 +53,21 @@ internal class DefaultStopLiveLocationShareTask @Inject constructor(
eventType = EventType.STATE_ROOM_BEACON_INFO.first(), eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
body = updatedContent body = updatedContent
) )
sendStateTask.executeRetry(sendStateTaskParams, 3) return try {
val eventId = sendStateTask.executeRetry(sendStateTaskParams, 3)
if (eventId.isNotEmpty()) {
UpdateLiveLocationShareResult.Success(eventId)
} else {
UpdateLiveLocationShareResult.Failure(Exception("empty event id for new state event"))
}
} catch (error: Throwable) {
UpdateLiveLocationShareResult.Failure(error)
}
} }
private fun getResultForIncorrectBeaconInfoEvent() =
UpdateLiveLocationShareResult.Failure(Exception("incorrect last beacon info event"))
private fun getLiveLocationBeaconInfoForUser(userId: String, roomId: String): Event? { private fun getLiveLocationBeaconInfoForUser(userId: String, roomId: String): Event? {
return EventType.STATE_ROOM_BEACON_INFO return EventType.STATE_ROOM_BEACON_INFO
.mapNotNull { .mapNotNull {

View File

@ -18,15 +18,14 @@ package org.matrix.android.sdk.internal.session.room.location
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs
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.Test import org.junit.Test
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.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper
@ -119,11 +118,11 @@ internal class DefaultLocationSharingServiceTest {
@Test @Test
fun `live location share can be started with a given timeout`() = runTest { fun `live location share can be started with a given timeout`() = runTest {
coEvery { startLiveLocationShareTask.execute(any()) } returns AN_EVENT_ID coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
val eventId = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
eventId shouldBeEqualTo AN_EVENT_ID result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
val expectedParams = StartLiveLocationShareTask.Params( val expectedParams = StartLiveLocationShareTask.Params(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
timeoutMillis = A_TIMEOUT timeoutMillis = A_TIMEOUT
@ -133,10 +132,11 @@ internal class DefaultLocationSharingServiceTest {
@Test @Test
fun `live location share can be stopped`() = runTest { fun `live location share can be stopped`() = runTest {
coEvery { stopLiveLocationShareTask.execute(any()) } just runs coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
defaultLocationSharingService.stopLiveLocationShare() val result = defaultLocationSharingService.stopLiveLocationShare()
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
val expectedParams = StopLiveLocationShareTask.Params( val expectedParams = StopLiveLocationShareTask.Params(
roomId = A_ROOM_ID roomId = A_ROOM_ID
) )

View File

@ -20,10 +20,12 @@ 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.amshove.kluent.shouldBeInstanceOf
import org.junit.After import org.junit.After
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.internal.session.room.state.SendStateTask
import org.matrix.android.sdk.test.fakes.FakeClock import org.matrix.android.sdk.test.fakes.FakeClock
@ -53,7 +55,7 @@ internal class DefaultStartLiveLocationShareTaskTest {
} }
@Test @Test
fun `given parameters when calling the task then it is correctly executed`() = runTest { fun `given parameters and no error when calling the task then result is success`() = runTest {
val params = StartLiveLocationShareTask.Params( val params = StartLiveLocationShareTask.Params(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
timeoutMillis = A_TIMEOUT timeoutMillis = A_TIMEOUT
@ -63,7 +65,7 @@ internal class DefaultStartLiveLocationShareTaskTest {
val result = defaultStartLiveLocationShareTask.execute(params) val result = defaultStartLiveLocationShareTask.execute(params)
result shouldBeEqualTo AN_EVENT_ID result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
val expectedBeaconContent = MessageBeaconInfoContent( val expectedBeaconContent = MessageBeaconInfoContent(
timeout = params.timeoutMillis, timeout = params.timeoutMillis,
isLive = true, isLive = true,
@ -80,4 +82,33 @@ internal class DefaultStartLiveLocationShareTaskTest {
remainingRetry = 3 remainingRetry = 3
) )
} }
@Test
fun `given parameters and an empty returned event id when calling the task then result is failure`() = runTest {
val params = StartLiveLocationShareTask.Params(
roomId = A_ROOM_ID,
timeoutMillis = A_TIMEOUT
)
fakeClock.givenEpoch(AN_EPOCH)
fakeSendStateTask.givenExecuteRetryReturns("")
val result = defaultStartLiveLocationShareTask.execute(params)
result shouldBeInstanceOf UpdateLiveLocationShareResult.Failure::class
}
@Test
fun `given parameters and error during event sending when calling the task then result is failure`() = runTest {
val params = StartLiveLocationShareTask.Params(
roomId = A_ROOM_ID,
timeoutMillis = A_TIMEOUT
)
fakeClock.givenEpoch(AN_EPOCH)
val error = Throwable()
fakeSendStateTask.givenExecuteRetryThrows(error)
val result = defaultStartLiveLocationShareTask.execute(params)
result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
}
} }

View File

@ -19,11 +19,15 @@ package org.matrix.android.sdk.internal.session.room.location
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.shouldBeInstanceOf
import org.junit.After import org.junit.After
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.internal.session.room.state.SendStateTask
import org.matrix.android.sdk.test.fakes.FakeSendStateTask import org.matrix.android.sdk.test.fakes.FakeSendStateTask
@ -53,7 +57,7 @@ class DefaultStopLiveLocationShareTaskTest {
} }
@Test @Test
fun `given parameters when calling the task then it is correctly executed`() = runTest { fun `given parameters and no error when calling the task then result is success`() = runTest {
val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID) val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
val currentStateEvent = Event( val currentStateEvent = Event(
stateKey = A_USER_ID, stateKey = A_USER_ID,
@ -66,8 +70,9 @@ class DefaultStopLiveLocationShareTaskTest {
fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent)
fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID) fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
defaultStopLiveLocationShareTask.execute(params) val result = defaultStopLiveLocationShareTask.execute(params)
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
val expectedBeaconContent = MessageBeaconInfoContent( val expectedBeaconContent = MessageBeaconInfoContent(
timeout = A_TIMEOUT, timeout = A_TIMEOUT,
isLive = false, isLive = false,
@ -89,4 +94,78 @@ class DefaultStopLiveLocationShareTaskTest {
stateKey = A_USER_ID stateKey = A_USER_ID
) )
} }
@Test
fun `given parameters and an incorrect current state event when calling the task then result is failure`() = runTest {
val incorrectCurrentStateEvents = listOf(
// no event
null,
// no stateKey
Event(
stateKey = null,
content = MessageBeaconInfoContent(
timeout = A_TIMEOUT,
isLive = true,
unstableTimestampMillis = AN_EPOCH
).toContent()
),
// incorrect content
Event(
stateKey = A_USER_ID,
content = MessageAudioContent(
msgType = "",
body = ""
).toContent()
)
)
incorrectCurrentStateEvents.forEach { currentStateEvent ->
fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent)
fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
val result = defaultStopLiveLocationShareTask.execute(params)
result shouldBeInstanceOf UpdateLiveLocationShareResult.Failure::class
}
}
@Test
fun `given parameters and an empty returned event id when calling the task then result is failure`() = runTest {
val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
val currentStateEvent = Event(
stateKey = A_USER_ID,
content = MessageBeaconInfoContent(
timeout = A_TIMEOUT,
isLive = true,
unstableTimestampMillis = AN_EPOCH
).toContent()
)
fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent)
fakeSendStateTask.givenExecuteRetryReturns("")
val result = defaultStopLiveLocationShareTask.execute(params)
result shouldBeInstanceOf UpdateLiveLocationShareResult.Failure::class
}
@Test
fun `given parameters and error during event sending when calling the task then result is failure`() = runTest {
val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
val currentStateEvent = Event(
stateKey = A_USER_ID,
content = MessageBeaconInfoContent(
timeout = A_TIMEOUT,
isLive = true,
unstableTimestampMillis = AN_EPOCH
).toContent()
)
fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent)
val error = Throwable()
fakeSendStateTask.givenExecuteRetryThrows(error)
val result = defaultStopLiveLocationShareTask.execute(params)
result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
}
} }

View File

@ -27,6 +27,10 @@ internal class FakeSendStateTask : SendStateTask by mockk() {
coEvery { executeRetry(any(), any()) } returns eventId coEvery { executeRetry(any(), any()) } returns eventId
} }
fun givenExecuteRetryThrows(error: Throwable) {
coEvery { executeRetry(any(), any()) } throws error
}
fun verifyExecuteRetry(params: SendStateTask.Params, remainingRetry: Int) { fun verifyExecuteRetry(params: SendStateTask.Params, remainingRetry: Int) {
coVerify { executeRetry(params, remainingRetry) } coVerify { executeRetry(params, remainingRetry) }
} }

View File

@ -27,7 +27,7 @@ internal class FakeStateEventDataSource {
val instance: StateEventDataSource = mockk() val instance: StateEventDataSource = mockk()
fun givenGetStateEventReturns(event: Event) { fun givenGetStateEventReturns(event: Event?) {
every { every {
instance.getStateEvent( instance.getStateEvent(
roomId = any(), roomId = any(),

View File

@ -1293,6 +1293,10 @@ class TimelineViewModel @AssistedInject constructor(
locationSharingServiceConnection.bind(this) locationSharingServiceConnection.bind(this)
} }
override fun onLocationServiceError(error: Throwable) {
_viewEvents.post(RoomDetailViewEvents.Failure(throwable = error, showInDialog = true))
}
override fun onCleared() { override fun onCleared() {
timeline.dispose() timeline.dispose()
timeline.removeAllListeners() timeline.removeAllListeners()

View File

@ -30,6 +30,7 @@ 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 timber.log.Timber import timber.log.Timber
import java.util.Timer import java.util.Timer
import java.util.TimerTask import java.util.TimerTask
@ -54,8 +55,9 @@ 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 var roomArgsMap = mutableMapOf<String, RoomArgs>() private val roomArgsMap = mutableMapOf<String, RoomArgs>()
private var timers = mutableListOf<Timer>() private val timers = mutableListOf<Timer>()
var callback: Callback? = null
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -89,19 +91,27 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
} }
private suspend fun sendStartingLiveBeaconInfo(session: Session, roomArgs: RoomArgs) { private suspend fun sendStartingLiveBeaconInfo(session: Session, roomArgs: RoomArgs) {
val beaconEventId = session val updateLiveResult = session
.getRoom(roomArgs.roomId) .getRoom(roomArgs.roomId)
?.locationSharingService() ?.locationSharingService()
?.startLiveLocationShare(timeoutMillis = roomArgs.durationMillis) ?.startLiveLocationShare(timeoutMillis = roomArgs.durationMillis)
beaconEventId updateLiveResult
?.takeUnless { it.isEmpty() } ?.let { result ->
?.let { when (result) {
roomArgsMap[it] = roomArgs is UpdateLiveLocationShareResult.Success -> {
locationTracker.requestLastKnownLocation() roomArgsMap[result.beaconEventId] = roomArgs
locationTracker.requestLastKnownLocation()
}
is UpdateLiveLocationShareResult.Failure -> {
callback?.onServiceError(result.error)
tryToDestroyMe()
}
}
} }
?: run { ?: run {
Timber.w("### LocationSharingService.sendStartingLiveBeaconInfo error, no received beacon info id") Timber.w("### LocationSharingService.sendStartingLiveBeaconInfo error, no received beacon info id")
tryToDestroyMe()
} }
} }
@ -123,28 +133,28 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
fun stopSharingLocation(roomId: String) { fun stopSharingLocation(roomId: String) {
Timber.i("### LocationSharingService.stopSharingLocation for $roomId") Timber.i("### LocationSharingService.stopSharingLocation for $roomId")
// Send a new beacon info state by setting live field as false launchInIO { session ->
sendStoppedBeaconInfo(roomId) 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) }
synchronized(roomArgsMap) { tryToDestroyMe()
val beaconIds = roomArgsMap }
.filter { it.value.roomId == roomId } }
.map { it.key } is UpdateLiveLocationShareResult.Failure -> callback?.onServiceError(result.error)
beaconIds.forEach { roomArgsMap.remove(it) } else -> Unit
if (roomArgsMap.isEmpty()) {
Timber.i("### LocationSharingService. Destroying self, time is up for all rooms")
destroyMe()
} }
} }
} }
private fun sendStoppedBeaconInfo(roomId: String) { private suspend fun sendStoppedBeaconInfo(session: Session, roomId: String): UpdateLiveLocationShareResult? {
launchInIO { session -> return session.getRoom(roomId)
session.getRoom(roomId) ?.locationSharingService()
?.locationSharingService() ?.stopLiveLocationShare()
?.stopLiveLocationShare()
}
} }
override fun onLocationUpdate(locationData: LocationData) { override fun onLocationUpdate(locationData: LocationData) {
@ -178,6 +188,13 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
stopSelf() stopSelf()
} }
private fun tryToDestroyMe() {
if (roomArgsMap.isEmpty()) {
Timber.i("### LocationSharingService. Destroying self, time is up for all rooms")
destroyMe()
}
}
private fun destroyMe() { private fun destroyMe() {
locationTracker.removeCallback(this) locationTracker.removeCallback(this)
timers.forEach { it.cancel() } timers.forEach { it.cancel() }
@ -209,6 +226,10 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
fun getService(): LocationSharingService = this@LocationSharingService fun getService(): LocationSharingService = this@LocationSharingService
} }
interface Callback {
fun onServiceError(error: Throwable)
}
companion object { companion object {
const val EXTRA_ROOM_ARGS = "EXTRA_ROOM_ARGS" const val EXTRA_ROOM_ARGS = "EXTRA_ROOM_ARGS"
} }

View File

@ -25,11 +25,12 @@ import javax.inject.Inject
class LocationSharingServiceConnection @Inject constructor( class LocationSharingServiceConnection @Inject constructor(
private val context: Context private val context: Context
) : ServiceConnection { ) : ServiceConnection, LocationSharingService.Callback {
interface Callback { interface Callback {
fun onLocationServiceRunning() fun onLocationServiceRunning()
fun onLocationServiceStopped() fun onLocationServiceStopped()
fun onLocationServiceError(error: Throwable)
} }
private var callback: Callback? = null private var callback: Callback? = null
@ -57,14 +58,21 @@ class LocationSharingServiceConnection @Inject constructor(
} }
override fun onServiceConnected(className: ComponentName, binder: IBinder) { override fun onServiceConnected(className: ComponentName, binder: IBinder) {
locationSharingService = (binder as LocationSharingService.LocalBinder).getService() locationSharingService = (binder as LocationSharingService.LocalBinder).getService().also {
it.callback = this
}
isBound = true isBound = true
callback?.onLocationServiceRunning() callback?.onLocationServiceRunning()
} }
override fun onServiceDisconnected(className: ComponentName) { override fun onServiceDisconnected(className: ComponentName) {
isBound = false isBound = false
locationSharingService?.callback = null
locationSharingService = null locationSharingService = null
callback?.onLocationServiceStopped() callback?.onLocationServiceStopped()
} }
override fun onServiceError(error: Throwable) {
callback?.onLocationServiceError(error)
}
} }

View File

@ -18,4 +18,6 @@ package im.vector.app.features.location.live.map
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
sealed interface LocationLiveMapViewEvents : VectorViewEvents sealed interface LocationLiveMapViewEvents : VectorViewEvents {
data class Error(val error: Throwable) : LocationLiveMapViewEvents
}

View File

@ -76,6 +76,8 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
observeViewEvents()
views.liveLocationBottomSheetRecyclerView.configureWith(bottomSheetController, hasFixedSize = false, disableItemAnimation = true) views.liveLocationBottomSheetRecyclerView.configureWith(bottomSheetController, hasFixedSize = false, disableItemAnimation = true)
bottomSheetController.callback = object : LiveLocationBottomSheetController.Callback { bottomSheetController.callback = object : LiveLocationBottomSheetController.Callback {
@ -89,6 +91,14 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
} }
} }
private fun observeViewEvents() {
viewModel.observeViewEvents { viewEvent ->
when (viewEvent) {
is LocationLiveMapViewEvents.Error -> displayErrorDialog(viewEvent.error)
}
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
setupMap() setupMap()

View File

@ -80,4 +80,8 @@ class LocationLiveMapViewModel @AssistedInject constructor(
override fun onLocationServiceStopped() { override fun onLocationServiceStopped() {
// NOOP // NOOP
} }
override fun onLocationServiceError(error: Throwable) {
_viewEvents.post(LocationLiveMapViewEvents.Error(error))
}
} }