diff --git a/changelog.d/5758.feature b/changelog.d/5758.feature new file mode 100644 index 0000000000..512b5c3868 --- /dev/null +++ b/changelog.d/5758.feature @@ -0,0 +1 @@ +Live Location Sharing - Update beacon info state event when sharing is ended \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index e9b0e4f676..f645f3ebf9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -66,6 +66,19 @@ interface StateService { */ suspend fun deleteAvatar() + /** + * Stops sharing live location in the room + * @param userId user id + */ + suspend fun stopLiveLocation(userId: String) + + /** + * Returns beacon info state event of a user + * @param userId user id who is sharing location + * @param filterOnlyLive filters only ongoing live location sharing beacons if true else ended event is included + */ + suspend fun getLiveLocationBeaconInfo(userId: String, filterOnlyLive: Boolean): Event? + /** * Send a state event to the room * @param eventType The type of event to send. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index 417417f439..89d33f98d2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -21,16 +21,20 @@ import androidx.lifecycle.LiveData import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent +import org.matrix.android.sdk.api.session.room.model.livelocation.BeaconInfo +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationBeaconContent import org.matrix.android.sdk.api.session.room.state.StateService import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.MimeTypes @@ -186,4 +190,42 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private } updateJoinRule(RoomJoinRules.RESTRICTED, null, allowEntries) } + + override suspend fun stopLiveLocation(userId: String) { + getLiveLocationBeaconInfo(userId, true)?.let { beaconInfoStateEvent -> + beaconInfoStateEvent.getClearContent()?.toModel()?.let { content -> + val beaconContent = LiveLocationBeaconContent( + unstableBeaconInfo = BeaconInfo( + description = content.getBestBeaconInfo()?.description, + timeout = content.getBestBeaconInfo()?.timeout, + isLive = false, + ), + unstableTimestampAsMilliseconds = System.currentTimeMillis() + ).toContent() + + beaconInfoStateEvent.stateKey?.let { + sendStateEvent( + eventType = EventType.STATE_ROOM_BEACON_INFO.first(), + body = beaconContent, + stateKey = it + ) + } + } + } + } + + override suspend fun getLiveLocationBeaconInfo(userId: String, filterOnlyLive: Boolean): Event? { + return EventType.STATE_ROOM_BEACON_INFO + .mapNotNull { + stateEventDataSource.getStateEvent( + roomId = roomId, + eventType = it, + stateKey = QueryStringValue.Equals(userId) + ) + } + .firstOrNull { beaconInfoEvent -> + !filterOnlyLive || + beaconInfoEvent.getClearContent()?.toModel()?.getBestBeaconInfo()?.isLive.orFalse() + } + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index d10b363519..f6ea8b76ef 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -111,4 +111,7 @@ sealed class RoomDetailAction : VectorViewModelAction { // Poll data class EndPoll(val eventId: String) : RoomDetailAction() + + // Live Location + object StopLiveLocationSharing : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index a722729629..a1163e0dd8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -385,6 +385,7 @@ class TimelineFragment @Inject constructor( setupEmojiButton() setupRemoveJitsiWidgetView() setupVoiceMessageView() + setupLiveLocationIndicator() views.includeRoomToolbar.roomToolbarContentView.debouncedClicks { navigator.openRoomProfile(requireActivity(), timelineArgs.roomId) @@ -810,6 +811,12 @@ class TimelineFragment @Inject constructor( } } + private fun setupLiveLocationIndicator() { + views.locationLiveStatusIndicator.stopButton.debouncedClicks { + timelineViewModel.handle(RoomDetailAction.StopLiveLocationSharing) + } + } + private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) { navigator.openRoomWidget(requireContext(), timelineArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo)) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 36ab526cd9..b79b55b248 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -444,6 +444,7 @@ class TimelineViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true)) } is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId) + RoomDetailAction.StopLiveLocationSharing -> handleStopLiveLocationSharing() } } @@ -1093,6 +1094,10 @@ class TimelineViewModel @AssistedInject constructor( } } + private fun handleStopLiveLocationSharing() { + locationSharingServiceConnection.stopLiveLocationSharing(room.roomId) + } + private fun observeRoomSummary() { room.flow().liveRoomSummary() .unwrap() diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 85679e34a7..2126cdac04 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -87,7 +87,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { .getSafeActiveSession() ?.let { session -> session.coroutineScope.launch(session.coroutineDispatchers.io) { - sendBeaconInfo(session, roomArgs) + sendLiveBeaconInfo(session, roomArgs) } } } @@ -95,7 +95,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { return START_STICKY } - private suspend fun sendBeaconInfo(session: Session, roomArgs: RoomArgs) { + private suspend fun sendLiveBeaconInfo(session: Session, roomArgs: RoomArgs) { val beaconContent = LiveLocationBeaconContent( unstableBeaconInfo = BeaconInfo( timeout = roomArgs.durationMillis, @@ -129,8 +129,12 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { } } - private fun stopSharingLocation(roomId: String) { + fun stopSharingLocation(roomId: String) { Timber.i("### LocationSharingService.stopSharingLocation for $roomId") + + // Send a new beacon info state by setting live field as false + sendStoppedBeaconInfo(roomId) + synchronized(roomArgsList) { roomArgsList.removeAll { it.roomId == roomId } if (roomArgsList.isEmpty()) { @@ -140,19 +144,39 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { } } + private fun sendStoppedBeaconInfo(roomId: String) { + activeSessionHolder + .getSafeActiveSession() + ?.let { session -> + session.coroutineScope.launch(session.coroutineDispatchers.io) { + session.getRoom(roomId)?.stopLiveLocation(session.myUserId) + } + } + } + override fun onLocationUpdate(locationData: LocationData) { Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}") + val session = activeSessionHolder.getSafeActiveSession() // Emit location update to all rooms in which live location sharing is active - roomArgsList.toList().forEach { roomArg -> - sendLiveLocation(roomArg.roomId, locationData) + session?.coroutineScope?.launch(session.coroutineDispatchers.io) { + roomArgsList.toList().forEach { roomArg -> + sendLiveLocation(roomArg.roomId, locationData) + } } } - private fun sendLiveLocation(roomId: String, locationData: LocationData) { - val room = activeSessionHolder.getSafeActiveSession()?.getRoom(roomId) + private suspend fun sendLiveLocation(roomId: String, locationData: LocationData) { + val session = activeSessionHolder.getSafeActiveSession() + val room = session?.getRoom(roomId) + val userId = session?.myUserId + + if (room == null || userId == null) { + return + } + room - ?.getStateEvent(EventType.STATE_ROOM_BEACON_INFO.first()) + .getLiveLocationBeaconInfo(userId, true) ?.eventId ?.let { room.sendLiveLocation( diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt index 9af6b1539a..e72f77531b 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt @@ -34,6 +34,7 @@ class LocationSharingServiceConnection @Inject constructor( private var callback: Callback? = null private var isBound = false + private var locationSharingService: LocationSharingService? = null fun bind(callback: Callback) { this.callback = callback @@ -51,13 +52,19 @@ class LocationSharingServiceConnection @Inject constructor( callback = null } + fun stopLiveLocationSharing(roomId: String) { + locationSharingService?.stopSharingLocation(roomId) + } + override fun onServiceConnected(className: ComponentName, binder: IBinder) { + locationSharingService = (binder as LocationSharingService.LocalBinder).getService() isBound = true callback?.onLocationServiceRunning() } override fun onServiceDisconnected(className: ComponentName) { isBound = false + locationSharingService = null callback?.onLocationServiceStopped() } }