From cf90ff9fd2bef649ca8dcfb2b78e23a667cf9a1d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 11 May 2022 14:07:05 +0200 Subject: [PATCH 01/31] Adding changelog entry --- changelog.d/6012.wip | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.d/6012.wip b/changelog.d/6012.wip index 9c67d562fe..783ca6d46a 100644 --- a/changelog.d/6012.wip +++ b/changelog.d/6012.wip @@ -1 +1,2 @@ Live location sharing: navigation from timeline to map screen +Live location sharing: show user pins on map screen From 44ca82bbef801ed29d68e5c6d621258738a23f68 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 11 May 2022 15:25:58 +0200 Subject: [PATCH 02/31] Adding ViewModel to Activity --- .../app/core/di/MavericksViewModelModule.kt | 6 +++ .../live/map/LocationLiveMapAction.kt | 21 ++++++++ .../live/map/LocationLiveMapViewEvents.kt | 21 ++++++++ .../live/map/LocationLiveMapViewModel.kt | 50 +++++++++++++++++++ .../live/map/LocationLiveMapViewState.kt | 34 +++++++++++++ 5 files changed, 132 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewEvents.kt create mode 100644 vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 33afcf1dfb..313fd7b98c 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -54,6 +54,7 @@ import im.vector.app.features.home.room.list.RoomListViewModel import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel import im.vector.app.features.invite.InviteUsersToRoomViewModel import im.vector.app.features.location.LocationSharingViewModel +import im.vector.app.features.location.live.map.LocationLiveMapViewModel import im.vector.app.features.login.LoginViewModel import im.vector.app.features.login2.LoginViewModel2 import im.vector.app.features.login2.created.AccountCreatedViewModel @@ -600,4 +601,9 @@ interface MavericksViewModelModule { @IntoMap @MavericksViewModelKey(VectorAttachmentViewerViewModel::class) fun vectorAttachmentViewerViewModelFactory(factory: VectorAttachmentViewerViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(LocationLiveMapViewModel::class) + fun locationLiveMapViewModelFactory(factory: LocationLiveMapViewModel.Factory): MavericksAssistedViewModelFactory<*, *> } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapAction.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapAction.kt new file mode 100644 index 0000000000..a31e02611f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapAction.kt @@ -0,0 +1,21 @@ +/* + * 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.map + +import im.vector.app.core.platform.VectorViewModelAction + +sealed interface LocationLiveMapAction : VectorViewModelAction diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewEvents.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewEvents.kt new file mode 100644 index 0000000000..6645ff58d9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewEvents.kt @@ -0,0 +1,21 @@ +/* + * 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.map + +import im.vector.app.core.platform.VectorViewEvents + +sealed interface LocationLiveMapViewEvents : VectorViewEvents diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt new file mode 100644 index 0000000000..e9b9cc8259 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt @@ -0,0 +1,50 @@ +/* + * 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.map + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +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.home.room.detail.timeline.helper.LocationPinProvider +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.getRoom + +class LocationLiveMapViewModel @AssistedInject constructor( + @Assisted private val initialState: LocationLiveMapViewState, +) : VectorViewModel(initialState) { + + // TODO create useCase to get Flow of user live location in room => Mock data for now + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: LocationLiveMapViewState): LocationLiveMapViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + init { + // TODO call use case to collect flow of user live location + } + + override fun handle(action: LocationLiveMapAction) { + // do nothing, no action for now + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt new file mode 100644 index 0000000000..d6e58b2486 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt @@ -0,0 +1,34 @@ +/* + * 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.map + +import com.airbnb.mvrx.MavericksState +import im.vector.app.features.location.LocationData + +data class LocationLiveMapViewState( + val roomId: String, + val userLocations: List = emptyList() +) : MavericksState { + constructor(locationLiveMapViewArgs: LocationLiveMapViewArgs) : this( + roomId = locationLiveMapViewArgs.roomId + ) +} + +data class UserLiveLocationViewState( + val userId: String, + val locationData: LocationData +) From d6029210d00f65ba99a9d1670e5817632779ea92 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 11 May 2022 16:15:45 +0200 Subject: [PATCH 03/31] Adding use case to get live location of users --- .../map/GetCurrentUserLiveLocationUseCase.kt | 49 +++++++++++++++++++ .../live/map/LocationLiveMapViewModel.kt | 13 ++--- 2 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/location/live/map/GetCurrentUserLiveLocationUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/GetCurrentUserLiveLocationUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/map/GetCurrentUserLiveLocationUseCase.kt new file mode 100644 index 0000000000..3f12fb9181 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/GetCurrentUserLiveLocationUseCase.kt @@ -0,0 +1,49 @@ +/* + * 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.map + +import im.vector.app.features.location.LocationData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class GetCurrentUserLiveLocationUseCase @Inject constructor() { + + // TODO add unit tests + fun execute(): Flow> { + // TODO get room and call SDK to get the correct flow + return flow { + val user1 = UserLiveLocationViewState( + userId = "user1", + locationData = LocationData( + latitude = 48.863447, + longitude = 2.328608, + uncertainty = null + ) + ) + val user2 = UserLiveLocationViewState( + userId = "user2", + locationData = LocationData( + latitude = 48.843816, + longitude = 2.359235, + uncertainty = null + ) + ) + emit(listOf(user1, user2)) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt index e9b9cc8259..c33e708d6b 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt @@ -23,16 +23,15 @@ import dagger.assisted.AssistedInject 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.home.room.detail.timeline.helper.LocationPinProvider -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.getRoom +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +// TODO add unit tests class LocationLiveMapViewModel @AssistedInject constructor( @Assisted private val initialState: LocationLiveMapViewState, + getCurrentUserLiveLocationUseCase: GetCurrentUserLiveLocationUseCase ) : VectorViewModel(initialState) { - // TODO create useCase to get Flow of user live location in room => Mock data for now - @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { override fun create(initialState: LocationLiveMapViewState): LocationLiveMapViewModel @@ -41,7 +40,9 @@ class LocationLiveMapViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { - // TODO call use case to collect flow of user live location + getCurrentUserLiveLocationUseCase.execute() + .onEach { setState { copy(userLocations = it) } } + .launchIn(viewModelScope) } override fun handle(action: LocationLiveMapAction) { From 5410b61ae32b9d645b789ecbbd79e54a037fbbfc Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 12 May 2022 09:27:01 +0200 Subject: [PATCH 04/31] Show user pins with correct zoom when map is first opened --- .../map/GetCurrentUserLiveLocationUseCase.kt | 49 ----------- .../map/GetListOfUserLiveLocationUseCase.kt | 66 ++++++++++++++ .../live/map/LocationLiveMapViewFragment.kt | 87 ++++++++++++++++++- .../live/map/LocationLiveMapViewModel.kt | 4 +- .../live/map/LocationLiveMapViewState.kt | 2 + 5 files changed, 155 insertions(+), 53 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/location/live/map/GetCurrentUserLiveLocationUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/GetCurrentUserLiveLocationUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/map/GetCurrentUserLiveLocationUseCase.kt deleted file mode 100644 index 3f12fb9181..0000000000 --- a/vector/src/main/java/im/vector/app/features/location/live/map/GetCurrentUserLiveLocationUseCase.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.map - -import im.vector.app.features.location.LocationData -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import javax.inject.Inject - -class GetCurrentUserLiveLocationUseCase @Inject constructor() { - - // TODO add unit tests - fun execute(): Flow> { - // TODO get room and call SDK to get the correct flow - return flow { - val user1 = UserLiveLocationViewState( - userId = "user1", - locationData = LocationData( - latitude = 48.863447, - longitude = 2.328608, - uncertainty = null - ) - ) - val user2 = UserLiveLocationViewState( - userId = "user2", - locationData = LocationData( - latitude = 48.843816, - longitude = 2.359235, - uncertainty = null - ) - ) - emit(listOf(user1, user2)) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt new file mode 100644 index 0000000000..2505ba11a4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt @@ -0,0 +1,66 @@ +/* + * 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.map + +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import im.vector.app.features.location.LocationData +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject + +class GetListOfUserLiveLocationUseCase @Inject constructor( + private val session: Session, + private val locationPinProvider: LocationPinProvider +) { + + // TODO add unit tests + fun execute(): Flow> { + // TODO get room and call SDK to get the correct flow of locations + + return callbackFlow { + val myUserId = session.myUserId + + locationPinProvider.create(myUserId) { pinDrawable -> + val user1 = UserLiveLocationViewState( + userId = session.myUserId, + pinDrawable = pinDrawable, + locationData = LocationData( + latitude = 48.863447, + longitude = 2.328608, + uncertainty = null + ) + ) + val user2 = UserLiveLocationViewState( + userId = session.myUserId, + pinDrawable = pinDrawable, + locationData = LocationData( + latitude = 48.843816, + longitude = 2.359235, + uncertainty = null + ) + ) + val userLocations = listOf(user1, user2) + trySendBlocking(userLocations) + channel.close() + } + awaitClose() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt index 32b87727d8..8ba4cdb5d2 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt @@ -16,20 +16,34 @@ package im.vector.app.features.location.live.map +import android.graphics.drawable.Drawable import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.constants.MapboxConstants +import com.mapbox.mapboxsdk.geometry.LatLng +import com.mapbox.mapboxsdk.geometry.LatLngBounds +import com.mapbox.mapboxsdk.maps.MapView +import com.mapbox.mapboxsdk.maps.MapboxMap import com.mapbox.mapboxsdk.maps.MapboxMapOptions +import com.mapbox.mapboxsdk.maps.Style import com.mapbox.mapboxsdk.maps.SupportMapFragment import dagger.hilt.android.AndroidEntryPoint +import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager +import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions +import com.mapbox.mapboxsdk.style.layers.Property import im.vector.app.R import im.vector.app.core.extensions.addChildFragment import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSimpleContainerBinding import im.vector.app.features.location.UrlMapProvider +import java.lang.ref.WeakReference import javax.inject.Inject /** @@ -43,6 +57,14 @@ class LocationLiveMapViewFragment : VectorBaseFragment? = null + private var symbolManager: SymbolManager? = null + private var mapStyle: Style? = null + private val pendingLiveLocations = mutableListOf() + private var isMapFirstUpdate = true + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSimpleContainerBinding { return FragmentSimpleContainerBinding.inflate(layoutInflater, container, false) } @@ -55,9 +77,70 @@ class LocationLiveMapViewFragment : VectorBaseFragment + mapFragment.getMapAsync { mapboxMap -> lifecycleScope.launchWhenCreated { - mapBoxMap.setStyle(urlMapProvider.getMapUrl()) + mapboxMap.setStyle(urlMapProvider.getMapUrl()) { style -> + mapStyle = style + this@LocationLiveMapViewFragment.mapboxMap = WeakReference(mapboxMap) + symbolManager = SymbolManager(mapFragment.view as MapView, mapboxMap, style) + pendingLiveLocations + .takeUnless { it.isEmpty() } + ?.let { updateMap(it) } + } + } + } + } + + override fun invalidate() = withState(viewModel) { viewState -> + updateMap(viewState.userLocations) + } + + private fun updateMap(userLiveLocations: List) { + symbolManager?.let { + it.deleteAll() + + val latLngBoundsBuilder = LatLngBounds.Builder() + userLiveLocations.forEach { userLocation -> + addUserPinToMapStyle(userLocation.userId, userLocation.pinDrawable) + val symbolOptions = buildSymbolOptions(userLocation) + it.create(symbolOptions) + + if (isMapFirstUpdate) { + latLngBoundsBuilder.include(LatLng(userLocation.locationData.latitude, userLocation.locationData.longitude)) + } + } + + if (isMapFirstUpdate) { + isMapFirstUpdate = false + zoomToViewAllUsers(latLngBoundsBuilder.build()) + } + } ?: run { + pendingLiveLocations.clear() + pendingLiveLocations.addAll(userLiveLocations) + } + } + + private fun addUserPinToMapStyle(userId: String, userPinDrawable: Drawable) { + mapStyle?.let { style -> + if (style.getImage(userId) == null) { + style.addImage(userId, userPinDrawable) + } + } + } + + private fun buildSymbolOptions(userLiveLocation: UserLiveLocationViewState) = + SymbolOptions() + .withLatLng(LatLng(userLiveLocation.locationData.latitude, userLiveLocation.locationData.longitude)) + .withIconImage(userLiveLocation.userId) + .withIconAnchor(Property.ICON_ANCHOR_BOTTOM) + + private fun zoomToViewAllUsers(latLngBounds: LatLngBounds) { + mapboxMap?.get()?.let { mapboxMap -> + mapboxMap.getCameraForLatLngBounds(latLngBounds)?.let { cameraPosition -> + // update the zoom a little to avoid having pins exactly at the edges of the map + mapboxMap.cameraPosition = CameraPosition.Builder(cameraPosition) + .zoom((cameraPosition.zoom - 1).coerceAtLeast(MapboxConstants.MINIMUM_ZOOM.toDouble())) + .build() } } } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt index c33e708d6b..bd834b4672 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.onEach // TODO add unit tests class LocationLiveMapViewModel @AssistedInject constructor( @Assisted private val initialState: LocationLiveMapViewState, - getCurrentUserLiveLocationUseCase: GetCurrentUserLiveLocationUseCase + getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase ) : VectorViewModel(initialState) { @AssistedFactory @@ -40,7 +40,7 @@ class LocationLiveMapViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { - getCurrentUserLiveLocationUseCase.execute() + getListOfUserLiveLocationUseCase.execute() .onEach { setState { copy(userLocations = it) } } .launchIn(viewModelScope) } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt index d6e58b2486..ca3be8e6ca 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt @@ -16,6 +16,7 @@ package im.vector.app.features.location.live.map +import android.graphics.drawable.Drawable import com.airbnb.mvrx.MavericksState import im.vector.app.features.location.LocationData @@ -30,5 +31,6 @@ data class LocationLiveMapViewState( data class UserLiveLocationViewState( val userId: String, + val pinDrawable: Drawable, val locationData: LocationData ) From 7a7af40d6178a8791ada152433db852ea7c66aee Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 16 May 2022 17:44:54 +0200 Subject: [PATCH 05/31] Creation of LocationSharingService to get current users sharing their live locations --- .../android/sdk/api/session/room/Room.kt | 6 +++ .../location/DefaultLocationSharingService.kt | 48 +++++++++++++++++++ .../room/location/LocationSharingService.kt | 27 +++++++++++ .../LiveLocationShareAggregatedSummary.kt | 1 + .../database/RealmSessionStoreMigration.kt | 4 +- .../mapper/EventAnnotationsSummaryMapper.kt | 2 +- ...iveLocationShareAggregatedSummaryMapper.kt | 5 +- .../database/migration/MigrateSessionTo029.kt | 38 +++++++++++++++ ...iveLocationShareAggregatedSummaryEntity.kt | 2 + ...cationShareAggregatedSummaryEntityQuery.kt | 20 +++++++- .../sdk/internal/session/room/DefaultRoom.kt | 3 ++ .../sdk/internal/session/room/RoomFactory.kt | 3 ++ 12 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/DefaultLocationSharingService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 3a18cf1497..85ccdeceff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataServic import org.matrix.android.sdk.api.session.room.alias.AliasService import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService +import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.members.MembershipService import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.relation.RelationService @@ -163,4 +164,9 @@ interface Room { * Get the RoomVersionService associated to this Room. */ fun roomVersionService(): RoomVersionService + + /** + * Get the LocationSharingService associated to this Room + */ + fun locationSharingService(): LocationSharingService } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/DefaultLocationSharingService.kt new file mode 100644 index 0000000000..abd6860309 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/DefaultLocationSharingService.kt @@ -0,0 +1,48 @@ +/* + * 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 + +import androidx.lifecycle.LiveData +import com.zhuinden.monarchy.Monarchy +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary +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.query.findRunningLiveLocationShareInRoom +import org.matrix.android.sdk.internal.di.SessionDatabase + +// TODO add unit tests +internal class DefaultLocationSharingService @AssistedInject constructor( + @Assisted private val roomId: String, + @SessionDatabase private val monarchy: Monarchy, + private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper, +) : LocationSharingService { + + @AssistedFactory + interface Factory { + fun create(roomId: String): DefaultLocationSharingService + } + + override fun getRunningLiveLocationShareSummaries(): LiveData> { + return monarchy.findAllMappedWithChanges( + { LiveLocationShareAggregatedSummaryEntity.findRunningLiveLocationShareInRoom(it, roomId = roomId) }, + { liveLocationShareAggregatedSummaryMapper.map(it) } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt new file mode 100644 index 0000000000..dd48d51f45 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -0,0 +1,27 @@ +/* + * 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 + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary + +/** + * Manage all location sharing related features. + */ +interface LocationSharingService { + fun getRunningLiveLocationShareSummaries(): LiveData> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt index 059fe21471..5ad1a48217 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocati * Aggregation info concerning a live location share. */ data class LiveLocationShareAggregatedSummary( + val userId: String?, /** * Indicate whether the live is currently running. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 55bccfd1ec..592461f927 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo028 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029 import org.matrix.android.sdk.internal.util.Normalizer import timber.log.Timber import javax.inject.Inject @@ -60,7 +61,7 @@ internal class RealmSessionStoreMigration @Inject constructor( override fun equals(other: Any?) = other is RealmSessionStoreMigration override fun hashCode() = 1000 - val schemaVersion = 28L + val schemaVersion = 29L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Realm Session from $oldVersion to $newVersion") @@ -93,5 +94,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 26) MigrateSessionTo026(realm).perform() if (oldVersion < 27) MigrateSessionTo027(realm).perform() if (oldVersion < 28) MigrateSessionTo028(realm).perform() + if (oldVersion < 29) MigrateSessionTo029(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt index c747ad334f..6bbeb17fdd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -58,7 +58,7 @@ internal object EventAnnotationsSummaryMapper { PollResponseAggregatedSummaryEntityMapper.map(it) }, liveLocationShareAggregatedSummary = annotationsSummary.liveLocationShareAggregatedSummary?.let { - LiveLocationShareAggregatedSummaryMapper.map(it) + LiveLocationShareAggregatedSummaryMapper().map(it) } ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt index 71b36f88bd..e8abcdb205 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt @@ -20,11 +20,14 @@ import org.matrix.android.sdk.api.session.events.model.toModel 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.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import javax.inject.Inject -internal object LiveLocationShareAggregatedSummaryMapper { +// TODO add unit tests +internal class LiveLocationShareAggregatedSummaryMapper @Inject constructor() { fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary { return LiveLocationShareAggregatedSummary( + userId = entity.userId, isActive = entity.isActive, endOfLiveTimestampMillis = entity.endOfLiveTimestampMillis, lastLocationDataContent = ContentMapper.map(entity.lastLocationContent).toModel() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt new file mode 100644 index 0000000000..fda7379726 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt @@ -0,0 +1,38 @@ +/* + * 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.internal.database.migration + +import io.realm.DynamicRealm +import io.realm.FieldAttribute +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * Migrating to: + * Live location sharing aggregated summary: adding new field userId + */ +internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 28) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("LiveLocationShareAggregatedSummaryEntity") + ?.addField(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, String::class.java, FieldAttribute.REQUIRED) + ?.transform { obj -> + obj.setString(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, "") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt index e84337693f..c5df8e9338 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt @@ -31,6 +31,8 @@ internal open class LiveLocationShareAggregatedSummaryEntity( var roomId: String = "", + var userId: String = "", + /** * Indicate whether the live is currently running. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt index 816b5f4392..9730a0ec16 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt @@ -20,6 +20,7 @@ import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields @@ -28,11 +29,17 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where( roomId: String, eventId: String, ): RealmQuery { - return realm.where() - .equalTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, roomId) + return LiveLocationShareAggregatedSummaryEntity + .whereRoomId(realm, roomId = roomId) .equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId) } +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.whereRoomId(realm: Realm, + roomId: String): RealmQuery { + return realm.where() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) +} + internal fun LiveLocationShareAggregatedSummaryEntity.Companion.create( realm: Realm, roomId: String, @@ -63,3 +70,12 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get( ): LiveLocationShareAggregatedSummaryEntity? { return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst() } + +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findRunningLiveLocationShareInRoom( + realm: Realm, + roomId: String, +): RealmQuery { + return LiveLocationShareAggregatedSummaryEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 7326adee4c..abea2d34cd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataServic import org.matrix.android.sdk.api.session.room.alias.AliasService import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService +import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.members.MembershipService import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType @@ -69,6 +70,7 @@ internal class DefaultRoom( private val roomAccountDataService: RoomAccountDataService, private val roomVersionService: RoomVersionService, private val viaParameterFinder: ViaParameterFinder, + private val locationSharingService: LocationSharingService, override val coroutineDispatchers: MatrixCoroutineDispatchers ) : Room { @@ -104,4 +106,5 @@ internal class DefaultRoom( override fun roomPushRuleService() = roomPushRuleService override fun roomAccountDataService() = roomAccountDataService override fun roomVersionService() = roomVersionService + override fun locationSharingService() = locationSharingService } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index 01c4fd1501..adfd55ca49 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.location.DefaultLocationSharingService import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder import org.matrix.android.sdk.internal.session.room.accountdata.DefaultRoomAccountDataService @@ -69,6 +70,7 @@ internal class DefaultRoomFactory @Inject constructor( private val roomVersionServiceFactory: DefaultRoomVersionService.Factory, private val roomAccountDataServiceFactory: DefaultRoomAccountDataService.Factory, private val viaParameterFinder: ViaParameterFinder, + private val locationSharingServiceFactory: DefaultLocationSharingService.Factory, private val coroutineDispatchers: MatrixCoroutineDispatchers ) : RoomFactory { @@ -96,6 +98,7 @@ internal class DefaultRoomFactory @Inject constructor( roomAccountDataService = roomAccountDataServiceFactory.create(roomId), roomVersionService = roomVersionServiceFactory.create(roomId), viaParameterFinder = viaParameterFinder, + locationSharingService = locationSharingServiceFactory.create(roomId), coroutineDispatchers = coroutineDispatchers ) } From 81b90df90983da0f8e71ed328f6b9fd5d98c64e6 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 16 May 2022 18:01:54 +0200 Subject: [PATCH 06/31] Observe the current live location shares in a room --- .../map/GetListOfUserLiveLocationUseCase.kt | 49 ++++------------ .../live/map/LocationLiveMapViewModel.kt | 2 +- .../map/UserLiveLocationViewStateMapper.kt | 56 +++++++++++++++++++ 3 files changed, 69 insertions(+), 38 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapper.kt diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt index 2505ba11a4..ca4f6859ee 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt @@ -16,51 +16,26 @@ package im.vector.app.features.location.live.map -import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider -import im.vector.app.features.location.LocationData -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.trySendBlocking +import androidx.lifecycle.asFlow import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.mapLatest import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.getRoom import javax.inject.Inject class GetListOfUserLiveLocationUseCase @Inject constructor( private val session: Session, - private val locationPinProvider: LocationPinProvider + private val userLiveLocationViewStateMapper: UserLiveLocationViewStateMapper, ) { // TODO add unit tests - fun execute(): Flow> { - // TODO get room and call SDK to get the correct flow of locations - - return callbackFlow { - val myUserId = session.myUserId - - locationPinProvider.create(myUserId) { pinDrawable -> - val user1 = UserLiveLocationViewState( - userId = session.myUserId, - pinDrawable = pinDrawable, - locationData = LocationData( - latitude = 48.863447, - longitude = 2.328608, - uncertainty = null - ) - ) - val user2 = UserLiveLocationViewState( - userId = session.myUserId, - pinDrawable = pinDrawable, - locationData = LocationData( - latitude = 48.843816, - longitude = 2.359235, - uncertainty = null - ) - ) - val userLocations = listOf(user1, user2) - trySendBlocking(userLocations) - channel.close() - } - awaitClose() - } + fun execute(roomId: String): Flow> { + return session.getRoom(roomId) + ?.locationSharingService() + ?.getRunningLiveLocationShareSummaries() + ?.asFlow() + ?.mapLatest { it.mapNotNull { summary -> userLiveLocationViewStateMapper.map(summary) } } + ?: emptyFlow() } } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt index bd834b4672..73608d35bc 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt @@ -40,7 +40,7 @@ class LocationLiveMapViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { - getListOfUserLiveLocationUseCase.execute() + getListOfUserLiveLocationUseCase.execute(initialState.roomId) .onEach { setState { copy(userLocations = it) } } .launchIn(viewModelScope) } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapper.kt b/vector/src/main/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapper.kt new file mode 100644 index 0000000000..fb5eb24769 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapper.kt @@ -0,0 +1,56 @@ +/* + * 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.map + +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import im.vector.app.features.location.toLocationData +import kotlinx.coroutines.suspendCancellableCoroutine +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary +import javax.inject.Inject + +// TODO add unit tests +class UserLiveLocationViewStateMapper @Inject constructor( + private val locationPinProvider: LocationPinProvider, +) { + + suspend fun map(liveLocationShareAggregatedSummary: LiveLocationShareAggregatedSummary) = + suspendCancellableCoroutine { continuation -> + val userId = liveLocationShareAggregatedSummary.userId + val locationData = liveLocationShareAggregatedSummary.lastLocationDataContent + ?.getBestLocationInfo() + ?.geoUri + .toLocationData() + + when { + userId.isNullOrEmpty() || locationData == null -> continuation.resume(null) { + // do nothing on cancellation + } + else -> { + locationPinProvider.create(userId) { pinDrawable -> + val viewState = UserLiveLocationViewState( + userId = userId, + pinDrawable = pinDrawable, + locationData = locationData + ) + continuation.resume(viewState) { + // do nothing on cancellation + } + } + } + } + } +} From 3b06f18ccb70eee30d8159d9bbddfbc15ee3fca3 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 17 May 2022 11:16:47 +0200 Subject: [PATCH 07/31] Remove unused imports --- .../sdk/internal/database/migration/MigrateSessionTo029.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt index fda7379726..44f4073144 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.database.migration import io.realm.DynamicRealm import io.realm.FieldAttribute -import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.util.database.RealmMigrator From ca9591e4237d025fd3f5f0057f26e5f098b4d598 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 17 May 2022 14:23:49 +0200 Subject: [PATCH 08/31] Fix set of userId in aggregation process --- .../aggregation/livelocation/LiveLocationAggregationProcessor.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt index 42dfc7ba9f..cbe41bccdb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt @@ -70,6 +70,7 @@ internal class LiveLocationAggregationProcessor @Inject constructor( val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) } aggregatedSummary.endOfLiveTimestampMillis = endOfLiveTimestampMillis aggregatedSummary.isActive = isLive + aggregatedSummary.userId = event.senderId if (isLive) { scheduleDeactivationAfterTimeout(targetEventId, roomId, endOfLiveTimestampMillis) From bec3f793f33cc60a598528e77946ee6b6eb05466 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 17 May 2022 14:24:31 +0200 Subject: [PATCH 09/31] Improve query of current running live location shares --- .../query/LiveLocationShareAggregatedSummaryEntityQuery.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt index 9730a0ec16..73da921d74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt @@ -35,7 +35,7 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where( } internal fun LiveLocationShareAggregatedSummaryEntity.Companion.whereRoomId(realm: Realm, - roomId: String): RealmQuery { + roomId: String): RealmQuery { return realm.where() .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) } @@ -78,4 +78,6 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findRunningLiveL return LiveLocationShareAggregatedSummaryEntity .whereRoomId(realm, roomId = roomId) .equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) + .isNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID) + .isNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT) } From 40d8d5c6054b0fcc2f9522e689608ea128cd3a7f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 17 May 2022 14:33:52 +0200 Subject: [PATCH 10/31] Updating user pins on location update --- .../location/LocationSharingFragment.kt | 2 +- .../app/features/location/MapBoxMapExt.kt | 39 ++++++ .../app/features/location/MapTilerMapView.kt | 10 +- .../live/map/LocationLiveMapViewFragment.kt | 114 ++++++++++++------ .../live/map/LocationLiveMapViewModel.kt | 5 + 5 files changed, 126 insertions(+), 44 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index cc5586e7f5..6de853519b 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -181,7 +181,7 @@ class LocationSharingFragment @Inject constructor( } private fun handleZoomToUserLocationEvent(event: LocationSharingViewEvents.ZoomToUserLocation) { - views.mapView.zoomToLocation(event.userLocation.latitude, event.userLocation.longitude) + views.mapView.zoomToLocation(event.userLocation) } private fun handleStartLiveLocationService(event: LocationSharingViewEvents.StartLiveLocationService) { diff --git a/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt b/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt new file mode 100644 index 0000000000..dbd2225909 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/MapBoxMapExt.kt @@ -0,0 +1,39 @@ +/* + * 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 + +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.constants.MapboxConstants +import com.mapbox.mapboxsdk.geometry.LatLng +import com.mapbox.mapboxsdk.geometry.LatLngBounds +import com.mapbox.mapboxsdk.maps.MapboxMap + +fun MapboxMap?.zoomToLocation(locationData: LocationData) { + this?.cameraPosition = CameraPosition.Builder() + .target(LatLng(locationData.latitude, locationData.longitude)) + .zoom(INITIAL_MAP_ZOOM_IN_PREVIEW) + .build() +} + +fun MapboxMap?.zoomToBounds(latLngBounds: LatLngBounds) { + this?.getCameraForLatLngBounds(latLngBounds)?.let { camPosition -> + // unZoom a little to avoid having pins exactly at the edges of the map + cameraPosition = CameraPosition.Builder(camPosition) + .zoom((camPosition.zoom - 1).coerceAtLeast(MapboxConstants.MINIMUM_ZOOM.toDouble())) + .build() + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt index 69e4e9fc20..dd2a56fb3a 100644 --- a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt +++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt @@ -25,7 +25,6 @@ import androidx.core.content.ContextCompat import androidx.core.view.marginBottom import androidx.core.view.marginTop import androidx.core.view.updateLayoutParams -import com.mapbox.mapboxsdk.camera.CameraPosition import com.mapbox.mapboxsdk.geometry.LatLng import com.mapbox.mapboxsdk.maps.MapView import com.mapbox.mapboxsdk.maps.MapboxMap @@ -164,7 +163,7 @@ class MapTilerMapView @JvmOverloads constructor( state.userLocationData?.let { locationData -> if (!initZoomDone || !state.zoomOnlyOnce) { - zoomToLocation(locationData.latitude, locationData.longitude) + zoomToLocation(locationData) initZoomDone = true } @@ -180,12 +179,9 @@ class MapTilerMapView @JvmOverloads constructor( } } - fun zoomToLocation(latitude: Double, longitude: Double) { + fun zoomToLocation(locationData: LocationData) { Timber.d("## Location: zoomToLocation") - mapRefs?.map?.cameraPosition = CameraPosition.Builder() - .target(LatLng(latitude, longitude)) - .zoom(INITIAL_MAP_ZOOM_IN_PREVIEW) - .build() + mapRefs?.map?.zoomToLocation(locationData) } fun getLocationOfMapCenter(): LocationData? = diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt index 8ba4cdb5d2..5e81ae7393 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt @@ -25,8 +25,6 @@ import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import com.mapbox.mapboxsdk.camera.CameraPosition -import com.mapbox.mapboxsdk.constants.MapboxConstants import com.mapbox.mapboxsdk.geometry.LatLng import com.mapbox.mapboxsdk.geometry.LatLngBounds import com.mapbox.mapboxsdk.maps.MapView @@ -43,11 +41,13 @@ import im.vector.app.core.extensions.addChildFragment import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSimpleContainerBinding import im.vector.app.features.location.UrlMapProvider +import im.vector.app.features.location.zoomToBounds +import im.vector.app.features.location.zoomToLocation import java.lang.ref.WeakReference import javax.inject.Inject /** - * Screen showing a map with all the current users sharing their live location in room. + * Screen showing a map with all the current users sharing their live location in a room. */ @AndroidEntryPoint class LocationLiveMapViewFragment : VectorBaseFragment() { @@ -91,35 +91,92 @@ class LocationLiveMapViewFragment : VectorBaseFragment updateMap(viewState.userLocations) } private fun updateMap(userLiveLocations: List) { - symbolManager?.let { - it.deleteAll() - + symbolManager?.let { sManager -> val latLngBoundsBuilder = LatLngBounds.Builder() + userLiveLocations.forEach { userLocation -> - addUserPinToMapStyle(userLocation.userId, userLocation.pinDrawable) - val symbolOptions = buildSymbolOptions(userLocation) - it.create(symbolOptions) + createOrUpdateSymbol(userLocation, sManager) if (isMapFirstUpdate) { - latLngBoundsBuilder.include(LatLng(userLocation.locationData.latitude, userLocation.locationData.longitude)) + val latLng = LatLng(userLocation.locationData.latitude, userLocation.locationData.longitude) + latLngBoundsBuilder.include(latLng) } } - if (isMapFirstUpdate) { - isMapFirstUpdate = false - zoomToViewAllUsers(latLngBoundsBuilder.build()) - } - } ?: run { - pendingLiveLocations.clear() - pendingLiveLocations.addAll(userLiveLocations) + removeOutdatedSymbols(userLiveLocations, sManager) + updateMapZoomWhenNeeded(userLiveLocations, latLngBoundsBuilder) + + } ?: postponeUpdateOfMap(userLiveLocations) + } + + private fun createOrUpdateSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) { + val symbolId = viewModel.mapSymbolIds[userLocation.userId] + + if (symbolId == null) { + createSymbol(userLocation, symbolManager) + } else { + updateSymbol(symbolId, userLocation, symbolManager) } } + private fun createSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) { + addUserPinToMapStyle(userLocation.userId, userLocation.pinDrawable) + val symbolOptions = buildSymbolOptions(userLocation) + val symbol = symbolManager.create(symbolOptions) + viewModel.mapSymbolIds[userLocation.userId] = symbol.id + } + + private fun updateSymbol(symbolId: Long, userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) { + val newLocation = LatLng(userLocation.locationData.latitude, userLocation.locationData.longitude) + val symbol = symbolManager.annotations.get(symbolId) + symbol?.let { + it.latLng = newLocation + symbolManager.update(it) + } + } + + private fun removeOutdatedSymbols(userLiveLocations: List, symbolManager: SymbolManager) { + val userIdsToRemove = viewModel.mapSymbolIds.keys.subtract(userLiveLocations.map { it.userId }.toSet()) + userIdsToRemove + .mapNotNull { userId -> + removeUserPinFromMapStyle(userId) + viewModel.mapSymbolIds[userId] + } + .forEach { symbolId -> + val symbol = symbolManager.annotations.get(symbolId) + symbolManager.delete(symbol) + } + } + + private fun updateMapZoomWhenNeeded(userLiveLocations: List, latLngBoundsBuilder: LatLngBounds.Builder) { + if (userLiveLocations.isNotEmpty() && isMapFirstUpdate) { + isMapFirstUpdate = false + if (userLiveLocations.size > 1) { + mapboxMap?.get()?.zoomToBounds(latLngBoundsBuilder.build()) + } else { + mapboxMap?.get()?.zoomToLocation(userLiveLocations.first().locationData) + } + } + } + + private fun postponeUpdateOfMap(userLiveLocations: List) { + pendingLiveLocations.clear() + pendingLiveLocations.addAll(userLiveLocations) + } + private fun addUserPinToMapStyle(userId: String, userPinDrawable: Drawable) { mapStyle?.let { style -> if (style.getImage(userId) == null) { @@ -128,31 +185,16 @@ class LocationLiveMapViewFragment : VectorBaseFragment - mapboxMap.getCameraForLatLngBounds(latLngBounds)?.let { cameraPosition -> - // update the zoom a little to avoid having pins exactly at the edges of the map - mapboxMap.cameraPosition = CameraPosition.Builder(cameraPosition) - .zoom((cameraPosition.zoom - 1).coerceAtLeast(MapboxConstants.MINIMUM_ZOOM.toDouble())) - .build() - } - } - } - - private fun getOrCreateSupportMapFragment() = - childFragmentManager.findFragmentByTag(MAP_FRAGMENT_TAG) as? SupportMapFragment - ?: run { - val options = MapboxMapOptions.createFromAttributes(requireContext(), null) - SupportMapFragment.newInstance(options) - .also { addChildFragment(R.id.fragmentContainer, it, tag = MAP_FRAGMENT_TAG) } - } - companion object { private const val MAP_FRAGMENT_TAG = "im.vector.app.features.location.live.map" } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt index 73608d35bc..1695055d72 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt @@ -39,6 +39,11 @@ class LocationLiveMapViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + /** + * Map to keep track of symbol ids associated to each user Id. + */ + val mapSymbolIds = mutableMapOf() + init { getListOfUserLiveLocationUseCase.execute(initialState.roomId) .onEach { setState { copy(userLocations = it) } } From 79212321a2d04141d7879188c0338c1df70fbd09 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 18 May 2022 09:22:19 +0200 Subject: [PATCH 11/31] Deactivate all previous active beacons when receiving one from user --- .../location/DefaultLocationSharingService.kt | 4 ++-- ...cationShareAggregatedSummaryEntityQuery.kt | 20 ++++++++++++++++--- .../LiveLocationAggregationProcessor.kt | 14 +++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/DefaultLocationSharingService.kt index abd6860309..196fec1100 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/DefaultLocationSharingService.kt @@ -24,7 +24,7 @@ import dagger.assisted.AssistedInject import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary 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.query.findRunningLiveLocationShareInRoom +import org.matrix.android.sdk.internal.database.query.findRunningLiveInRoom import org.matrix.android.sdk.internal.di.SessionDatabase // TODO add unit tests @@ -41,7 +41,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor( override fun getRunningLiveLocationShareSummaries(): LiveData> { return monarchy.findAllMappedWithChanges( - { LiveLocationShareAggregatedSummaryEntity.findRunningLiveLocationShareInRoom(it, roomId = roomId) }, + { LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) }, { liveLocationShareAggregatedSummaryMapper.map(it) } ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt index 73da921d74..67d481b48a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt @@ -20,7 +20,6 @@ import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity -import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields @@ -37,7 +36,7 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where( internal fun LiveLocationShareAggregatedSummaryEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery { return realm.where() - .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .equalTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, roomId) } internal fun LiveLocationShareAggregatedSummaryEntity.Companion.create( @@ -71,7 +70,22 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get( return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst() } -internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findRunningLiveLocationShareInRoom( +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveInRoomForUser( + realm: Realm, + roomId: String, + userId: String, +): List { + return LiveLocationShareAggregatedSummaryEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, userId) + .equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) + .findAll() +} + +/** + * A live is considered as running when active and with at least a last known location. + */ +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findRunningLiveInRoom( realm: Realm, roomId: String, ): RealmQuery { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt index cbe41bccdb..487786e69f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoCo import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.query.findActiveLiveInRoomForUser import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider @@ -72,6 +73,8 @@ internal class LiveLocationAggregationProcessor @Inject constructor( aggregatedSummary.isActive = isLive aggregatedSummary.userId = event.senderId + deactivateAllPreviousBeacons(realm, roomId, event.senderId, targetEventId) + if (isLive) { scheduleDeactivationAfterTimeout(targetEventId, roomId, endOfLiveTimestampMillis) } else { @@ -138,5 +141,16 @@ internal class LiveLocationAggregationProcessor @Inject constructor( } } + private fun deactivateAllPreviousBeacons(realm: Realm, roomId: String, userId: String, currentEventId: String) { + LiveLocationShareAggregatedSummaryEntity + .findActiveLiveInRoomForUser( + realm = realm, + roomId = roomId, + userId = userId + ) + .filterNot { it.eventId == currentEventId } + .forEach { it.isActive = false } + } + private fun Long.isMoreRecentThan(timestamp: Long) = this > timestamp } From bd473375a19c8022f8075a60c33559d7d665e55d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 18 May 2022 14:43:53 +0200 Subject: [PATCH 12/31] Fix no text visible if using direct pin drawable --- .../features/location/live/map/LocationLiveMapViewFragment.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt index 5e81ae7393..953c399c7f 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt @@ -21,6 +21,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.graphics.drawable.toBitmap import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel @@ -180,7 +181,7 @@ class LocationLiveMapViewFragment : VectorBaseFragment if (style.getImage(userId) == null) { - style.addImage(userId, userPinDrawable) + style.addImage(userId, userPinDrawable.toBitmap()) } } } From 401027e91926e2204da9a83d19f908f691c0b155 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 18 May 2022 17:25:38 +0200 Subject: [PATCH 13/31] Adding end of live timestamp into view state --- .../features/location/live/map/LocationLiveMapViewState.kt | 3 ++- .../location/live/map/UserLiveLocationViewStateMapper.kt | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt index ca3be8e6ca..9fa8635d82 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt @@ -32,5 +32,6 @@ data class LocationLiveMapViewState( data class UserLiveLocationViewState( val userId: String, val pinDrawable: Drawable, - val locationData: LocationData + val locationData: LocationData, + val endOfLiveTimestampMillis: Long? ) diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapper.kt b/vector/src/main/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapper.kt index fb5eb24769..8790144040 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapper.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapper.kt @@ -22,7 +22,6 @@ import kotlinx.coroutines.suspendCancellableCoroutine import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import javax.inject.Inject -// TODO add unit tests class UserLiveLocationViewStateMapper @Inject constructor( private val locationPinProvider: LocationPinProvider, ) { @@ -44,7 +43,8 @@ class UserLiveLocationViewStateMapper @Inject constructor( val viewState = UserLiveLocationViewState( userId = userId, pinDrawable = pinDrawable, - locationData = locationData + locationData = locationData, + endOfLiveTimestampMillis = liveLocationShareAggregatedSummary.endOfLiveTimestampMillis ) continuation.resume(viewState) { // do nothing on cancellation From 7ef91ce7175fc1b4eb763b7a455cd5bdba31ea99 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 18 May 2022 17:25:52 +0200 Subject: [PATCH 14/31] Adding unit tests for view state mapper --- .../UserLiveLocationViewStateMapperTest.kt | 106 ++++++++++++++++++ .../app/test/fakes/FakeLocationPinProvider.kt | 32 ++++++ 2 files changed, 138 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeLocationPinProvider.kt diff --git a/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt b/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt new file mode 100644 index 0000000000..7f83f99b98 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt @@ -0,0 +1,106 @@ +/* + * 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.map + +import android.graphics.drawable.Drawable +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.toLocationData +import im.vector.app.test.fakes.FakeLocationPinProvider +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals +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.LocationInfo +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent + +class UserLiveLocationViewStateMapperTest { + + private val locationPinProvider = FakeLocationPinProvider() + + private val userLiveLocationViewStateMapper = UserLiveLocationViewStateMapper(locationPinProvider.instance) + + @Before + fun setUp() { + mockkStatic("im.vector.app.features.location.LocationDataKt") + } + + @After + fun tearDown() { + unmockkStatic("im.vector.app.features.location.LocationDataKt") + } + + @Test + fun `given a summary with invalid data then result is null`() = runTest { + val summary1 = LiveLocationShareAggregatedSummary( + userId = null, + isActive = true, + endOfLiveTimestampMillis = null, + lastLocationDataContent = null, + ) + val summary2 = summary1.copy(userId = "") + val summaryWithoutLocation = summary1.copy(userId = "userId") + + val viewState1 = userLiveLocationViewStateMapper.map(summary1) + val viewState2 = userLiveLocationViewStateMapper.map(summary2) + val viewState3 = userLiveLocationViewStateMapper.map(summaryWithoutLocation) + + assertEquals(null, viewState1) + assertEquals(null, viewState2) + assertEquals(null, viewState3) + } + + @Test + fun `given a summary with valid data then result is correctly mapped`() = runTest { + val geoUri = "geoUri" + val userId = "userId" + val pinDrawable = mockk() + val endOfLiveTimestampMillis = 123L + + val locationDataContent = MessageBeaconLocationDataContent( + locationInfo = LocationInfo(geoUri = geoUri) + ) + val summary = LiveLocationShareAggregatedSummary( + userId = userId, + isActive = true, + endOfLiveTimestampMillis = endOfLiveTimestampMillis, + lastLocationDataContent = locationDataContent, + ) + val locationData = LocationData( + latitude = 1.0, + longitude = 2.0, + uncertainty = null + ) + every { geoUri.toLocationData() } returns locationData + locationPinProvider.givenCreateForUserId(userId, pinDrawable) + + val viewState = userLiveLocationViewStateMapper.map(summary) + + val expectedViewState = UserLiveLocationViewState( + userId = userId, + pinDrawable = pinDrawable, + locationData = locationData, + endOfLiveTimestampMillis = endOfLiveTimestampMillis + ) + assertEquals(expectedViewState, viewState) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationPinProvider.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationPinProvider.kt new file mode 100644 index 0000000000..9688f30927 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationPinProvider.kt @@ -0,0 +1,32 @@ +/* + * 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 android.graphics.drawable.Drawable +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import io.mockk.every +import io.mockk.invoke +import io.mockk.mockk + +class FakeLocationPinProvider { + + val instance = mockk(relaxed = true) + + fun givenCreateForUserId(userId: String, expectedDrawable: Drawable) { + every { instance.create(userId, captureLambda()) } answers { lambda<(Drawable) -> Unit>().invoke(expectedDrawable)} + } +} From c07bc0890f33773283b70fa3d321b2da426993e7 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 23 May 2022 10:04:41 +0200 Subject: [PATCH 15/31] WIP - unit tests --- ...iveLocationShareAggregatedSummaryMapper.kt | 1 - ...ocationShareAggregatedSummaryMapperTest.kt | 76 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt index e8abcdb205..9460e4c6ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt @@ -22,7 +22,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocati import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import javax.inject.Inject -// TODO add unit tests internal class LiveLocationShareAggregatedSummaryMapper @Inject constructor() { fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt new file mode 100644 index 0000000000..6209a9d65f --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt @@ -0,0 +1,76 @@ +/* + * 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 org.matrix.android.sdk.internal.database.mapper + +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import org.amshove.kluent.internal.assertEquals +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.toModel +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.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity + +class LiveLocationShareAggregatedSummaryMapperTest { + + private val mapper = LiveLocationShareAggregatedSummaryMapper() + + @Before + fun setUp() { + mockkStatic("org.matrix.android.sdk.internal.database.mapper.ContentMapperKt") + mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt") + } + + @After + fun tearDown() { + unmockkStatic("org.matrix.android.sdk.internal.database.mapper.ContentMapperKt") + unmockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt") + } + + @Test + fun `given an entity then result should be mapped correctly`() { + val userId = "userId" + val timeout = 123L + val isActive = true + val lastKnownLocationContent = "lastKnownLocationContent" + val messageBeaconLocationDataContent = MessageBeaconLocationDataContent() + val entity = LiveLocationShareAggregatedSummaryEntity( + userId = userId, + isActive = isActive, + endOfLiveTimestampMillis = timeout, + lastLocationContent = lastKnownLocationContent + ) + val content = mockk() + every { ContentMapper.map(lastKnownLocationContent) } returns content + every { content.toModel() } returns messageBeaconLocationDataContent + + val summary = mapper.map(entity) + + val expectedSummary = LiveLocationShareAggregatedSummary( + userId = userId, + isActive = isActive, + endOfLiveTimestampMillis = timeout, + lastLocationDataContent = messageBeaconLocationDataContent + ) + assertEquals(expectedSummary, summary) + } +} From 8145049315c644d3acb041141856f19696783ad1 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 23 May 2022 17:19:29 +0200 Subject: [PATCH 16/31] Fix potential access to null value --- .../location/live/map/LocationLiveMapViewFragment.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt index 953c399c7f..ecbdf7e39c 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt @@ -33,10 +33,10 @@ import com.mapbox.mapboxsdk.maps.MapboxMap import com.mapbox.mapboxsdk.maps.MapboxMapOptions import com.mapbox.mapboxsdk.maps.Style import com.mapbox.mapboxsdk.maps.SupportMapFragment -import dagger.hilt.android.AndroidEntryPoint import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions import com.mapbox.mapboxsdk.style.layers.Property +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.addChildFragment import im.vector.app.core.platform.VectorBaseFragment @@ -44,6 +44,7 @@ import im.vector.app.databinding.FragmentSimpleContainerBinding import im.vector.app.features.location.UrlMapProvider import im.vector.app.features.location.zoomToBounds import im.vector.app.features.location.zoomToLocation +import timber.log.Timber import java.lang.ref.WeakReference import javax.inject.Inject @@ -155,10 +156,13 @@ class LocationLiveMapViewFragment : VectorBaseFragment removeUserPinFromMapStyle(userId) viewModel.mapSymbolIds[userId] + viewModel.mapSymbolIds.remove(userId) } .forEach { symbolId -> - val symbol = symbolManager.annotations.get(symbolId) - symbolManager.delete(symbol) + Timber.d("trying to delete symbol with id: $symbolId") + symbolManager.annotations.get(symbolId)?.let { + symbolManager.delete(it) + } } } From 5efe26c7ddfcd2d7e9ac88466e99f89341bacd21 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 24 May 2022 09:24:10 +0200 Subject: [PATCH 17/31] Fix code quality issues --- .../mapper/LiveLocationShareAggregatedSummaryMapperTest.kt | 2 +- .../features/location/live/map/LocationLiveMapViewFragment.kt | 1 - .../java/im/vector/app/test/fakes/FakeLocationPinProvider.kt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt index 6209a9d65f..85b80f0a6e 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt index ecbdf7e39c..46c238ad12 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt @@ -120,7 +120,6 @@ class LocationLiveMapViewFragment : VectorBaseFragment(relaxed = true) fun givenCreateForUserId(userId: String, expectedDrawable: Drawable) { - every { instance.create(userId, captureLambda()) } answers { lambda<(Drawable) -> Unit>().invoke(expectedDrawable)} + every { instance.create(userId, captureLambda()) } answers { lambda<(Drawable) -> Unit>().invoke(expectedDrawable) } } } From d9480bb1360a0bf72527a0deb3b5377143220518 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 24 May 2022 16:40:38 +0200 Subject: [PATCH 18/31] Adding todo to add unit tests on aggregation process --- .../aggregation/livelocation/LiveLocationAggregationProcessor.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt index 487786e69f..0317de682a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt @@ -36,6 +36,7 @@ import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject +// TODO add unit tests internal class LiveLocationAggregationProcessor @Inject constructor( @SessionId private val sessionId: String, private val workManagerProvider: WorkManagerProvider, From 095cc12e10e0a40d77f1276f26c288d5920a0b35 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 24 May 2022 16:40:52 +0200 Subject: [PATCH 19/31] Fixing unit tests of the mapper --- ...LiveLocationShareAggregatedSummaryMapperTest.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt index 85b80f0a6e..1d070863b8 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt @@ -18,7 +18,9 @@ package org.matrix.android.sdk.internal.database.mapper import io.mockk.every import io.mockk.mockk +import io.mockk.mockkObject import io.mockk.mockkStatic +import io.mockk.unmockkObject import io.mockk.unmockkStatic import org.amshove.kluent.internal.assertEquals import org.junit.After @@ -36,14 +38,12 @@ class LiveLocationShareAggregatedSummaryMapperTest { @Before fun setUp() { - mockkStatic("org.matrix.android.sdk.internal.database.mapper.ContentMapperKt") - mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt") + mockkObject(ContentMapper) } @After fun tearDown() { - unmockkStatic("org.matrix.android.sdk.internal.database.mapper.ContentMapperKt") - unmockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt") + unmockkObject(ContentMapper) } @Test @@ -52,7 +52,6 @@ class LiveLocationShareAggregatedSummaryMapperTest { val timeout = 123L val isActive = true val lastKnownLocationContent = "lastKnownLocationContent" - val messageBeaconLocationDataContent = MessageBeaconLocationDataContent() val entity = LiveLocationShareAggregatedSummaryEntity( userId = userId, isActive = isActive, @@ -61,15 +60,16 @@ class LiveLocationShareAggregatedSummaryMapperTest { ) val content = mockk() every { ContentMapper.map(lastKnownLocationContent) } returns content - every { content.toModel() } returns messageBeaconLocationDataContent val summary = mapper.map(entity) + // note: unfortunately the implementation relies on an inline method to map the lastLocationDataContent + // since inline methods do not produce bytecode, it is not mockable and the verification on this field cannot be done val expectedSummary = LiveLocationShareAggregatedSummary( userId = userId, isActive = isActive, endOfLiveTimestampMillis = timeout, - lastLocationDataContent = messageBeaconLocationDataContent + lastLocationDataContent = null ) assertEquals(expectedSummary, summary) } From 65d7ec86964071b7027c333122dbfbbb8dd6d87d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 24 May 2022 17:45:22 +0200 Subject: [PATCH 20/31] Adding unit tests for use case to get the list of current running lives --- .../map/GetListOfUserLiveLocationUseCase.kt | 1 - .../GetListOfUserLiveLocationUseCaseTest.kt | 110 ++++++++++++++++++ .../test/fakes/FakeLocationSharingService.kt | 34 ++++++ .../java/im/vector/app/test/fakes/FakeRoom.kt | 27 +++++ .../vector/app/test/fakes/FakeRoomService.kt | 27 +++++ .../im/vector/app/test/fakes/FakeSession.kt | 4 +- 6 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt index ca4f6859ee..91f6999e2c 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt @@ -29,7 +29,6 @@ class GetListOfUserLiveLocationUseCase @Inject constructor( private val userLiveLocationViewStateMapper: UserLiveLocationViewStateMapper, ) { - // TODO add unit tests fun execute(roomId: String): Flow> { return session.getRoom(roomId) ?.locationSharingService() diff --git a/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt new file mode 100644 index 0000000000..765eee4937 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt @@ -0,0 +1,110 @@ +/* + * 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.map + +import androidx.lifecycle.asFlow +import com.airbnb.mvrx.test.MvRxTestRule +import im.vector.app.features.location.LocationData +import im.vector.app.test.fakes.FakeSession +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals +import org.junit.After +import org.junit.Before +import org.junit.Rule +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 + +class GetListOfUserLiveLocationUseCaseTest { + + @get:Rule + val mvRxTestRule = MvRxTestRule() + + private val fakeSession = FakeSession() + + private val viewStateMapper = mockk() + + private val getListOfUserLiveLocationUseCase = GetListOfUserLiveLocationUseCase(fakeSession, viewStateMapper) + + @Before + fun setUp() { + mockkStatic("androidx.lifecycle.FlowLiveDataConversions") + } + + @After + fun tearDown() { + unmockkStatic("androidx.lifecycle.FlowLiveDataConversions") + } + + @Test + fun `given a room id then the correct flow of view states list is collected`() = runTest { + val roomId = "roomId" + + val summary1 = LiveLocationShareAggregatedSummary( + userId = "userId1", + isActive = true, + endOfLiveTimestampMillis = 123, + lastLocationDataContent = MessageBeaconLocationDataContent() + ) + val summary2 = LiveLocationShareAggregatedSummary( + userId = "userId2", + isActive = true, + endOfLiveTimestampMillis = 1234, + lastLocationDataContent = MessageBeaconLocationDataContent() + ) + val summary3 = LiveLocationShareAggregatedSummary( + userId = "userId3", + isActive = true, + endOfLiveTimestampMillis = 1234, + lastLocationDataContent = MessageBeaconLocationDataContent() + ) + val summaries = listOf(summary1, summary2, summary3) + val liveData = fakeSession.roomService() + .getRoom(roomId) + .locationSharingService() + .givenRunningLiveLocationShareSummaries(summaries) + + every { liveData.asFlow() } returns flowOf(summaries) + + val viewState1 = UserLiveLocationViewState( + userId = "userId1", + pinDrawable = mockk(), + locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null), + endOfLiveTimestampMillis = 123 + ) + val viewState2 = UserLiveLocationViewState( + userId = "userId2", + pinDrawable = mockk(), + locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null), + endOfLiveTimestampMillis = 1234 + ) + coEvery { viewStateMapper.map(summary1) } returns viewState1 + coEvery { viewStateMapper.map(summary2) } returns viewState2 + coEvery { viewStateMapper.map(summary3) } returns null + + val viewStates = getListOfUserLiveLocationUseCase.execute(roomId).first() + + assertEquals(listOf(viewState1, viewState2), viewStates) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt new file mode 100644 index 0000000000..2cd98c086c --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021 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.MutableLiveData +import io.mockk.every +import io.mockk.mockk +import org.matrix.android.sdk.api.session.room.location.LocationSharingService +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary + +class FakeLocationSharingService : LocationSharingService by mockk() { + + fun givenRunningLiveLocationShareSummaries(summaries: List): + LiveData> { + return MutableLiveData(summaries).also { + every { getRunningLiveLocationShareSummaries() } returns it + } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt new file mode 100644 index 0000000000..ff87ab0fde --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 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 io.mockk.mockk +import org.matrix.android.sdk.api.session.room.Room + +class FakeRoom( + private val fakeLocationSharingService: FakeLocationSharingService = FakeLocationSharingService(), +) : Room by mockk() { + + override fun locationSharingService() = fakeLocationSharingService +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt new file mode 100644 index 0000000000..b09256f747 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 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 io.mockk.mockk +import org.matrix.android.sdk.api.session.room.RoomService + +class FakeRoomService( + private val fakeRoom: FakeRoom = FakeRoom() +) : RoomService by mockk() { + + override fun getRoom(roomId: String) = fakeRoom +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt index 5f02879e65..cf94493f61 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt @@ -33,7 +33,8 @@ class FakeSession( val fakeCryptoService: FakeCryptoService = FakeCryptoService(), val fakeProfileService: FakeProfileService = FakeProfileService(), val fakeHomeServerCapabilitiesService: FakeHomeServerCapabilitiesService = FakeHomeServerCapabilitiesService(), - val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService() + val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService(), + private val fakeRoomService: FakeRoomService = FakeRoomService(), ) : Session by mockk(relaxed = true) { init { @@ -48,6 +49,7 @@ class FakeSession( override fun profileService(): ProfileService = fakeProfileService override fun homeServerCapabilitiesService(): HomeServerCapabilitiesService = fakeHomeServerCapabilitiesService override fun sharedSecretStorageService() = fakeSharedSecretStorageService + override fun roomService() = fakeRoomService fun givenVectorStore(vectorSessionStore: VectorSessionStore) { coEvery { From 1b8440d7c85ff1ff96031ea726d3d138e82f96bf Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 24 May 2022 17:52:48 +0200 Subject: [PATCH 21/31] Removing unused imports --- .../mapper/LiveLocationShareAggregatedSummaryMapperTest.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt index 1d070863b8..f8b3b39350 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt @@ -19,17 +19,13 @@ package org.matrix.android.sdk.internal.database.mapper import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject -import io.mockk.mockkStatic import io.mockk.unmockkObject -import io.mockk.unmockkStatic import org.amshove.kluent.internal.assertEquals import org.junit.After import org.junit.Before import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.toModel 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.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity class LiveLocationShareAggregatedSummaryMapperTest { From 8d2debf47ea86263643043a0bbf12efe1d50b2e3 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 25 May 2022 09:44:07 +0200 Subject: [PATCH 22/31] Adding missing ending dots in comments --- .../main/java/org/matrix/android/sdk/api/session/room/Room.kt | 2 +- .../sdk/internal/database/migration/MigrateSessionTo029.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 85ccdeceff..5d2769ac3c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -166,7 +166,7 @@ interface Room { fun roomVersionService(): RoomVersionService /** - * Get the LocationSharingService associated to this Room + * Get the LocationSharingService associated to this Room. */ fun locationSharingService(): LocationSharingService } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt index 44f4073144..aebca11c2b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt @@ -23,7 +23,7 @@ import org.matrix.android.sdk.internal.util.database.RealmMigrator /** * Migrating to: - * Live location sharing aggregated summary: adding new field userId + * Live location sharing aggregated summary: adding new field userId. */ internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 28) { From 33151eef73a90cf43ec3dfe7f654968b1b91699b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 25 May 2022 10:54:09 +0200 Subject: [PATCH 23/31] Move the default implementation of location sharing service into internal package --- .../matrix/android/sdk/internal/session/room/RoomFactory.kt | 2 +- .../session/room/location/DefaultLocationSharingService.kt | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{api => internal}/session/room/location/DefaultLocationSharingService.kt (91%) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index adfd55ca49..512b88e3e0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.room.Room -import org.matrix.android.sdk.api.session.room.location.DefaultLocationSharingService +import org.matrix.android.sdk.internal.session.room.location.DefaultLocationSharingService import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder import org.matrix.android.sdk.internal.session.room.accountdata.DefaultRoomAccountDataService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt similarity index 91% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/DefaultLocationSharingService.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index 196fec1100..cecad77f06 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * 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. @@ -14,13 +14,14 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.session.room.location +package org.matrix.android.sdk.internal.session.room.location import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity From aa65d8234139a16baafcb76d32d492e188a2c615 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 25 May 2022 14:01:36 +0200 Subject: [PATCH 24/31] Adding unit tests for ViewModel --- .../live/map/LocationLiveMapViewModel.kt | 1 - .../live/map/LocationLiveMapViewModelTest.kt | 70 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 vector/src/test/java/im/vector/app/features/location/live/map/LocationLiveMapViewModelTest.kt diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt index 1695055d72..8c5f292f25 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt @@ -26,7 +26,6 @@ import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -// TODO add unit tests class LocationLiveMapViewModel @AssistedInject constructor( @Assisted private val initialState: LocationLiveMapViewState, getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase diff --git a/vector/src/test/java/im/vector/app/features/location/live/map/LocationLiveMapViewModelTest.kt b/vector/src/test/java/im/vector/app/features/location/live/map/LocationLiveMapViewModelTest.kt new file mode 100644 index 0000000000..330cedf986 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/location/live/map/LocationLiveMapViewModelTest.kt @@ -0,0 +1,70 @@ +/* + * 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.map + +import com.airbnb.mvrx.test.MvRxTestRule +import im.vector.app.features.location.LocationData +import im.vector.app.test.test +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class LocationLiveMapViewModelTest { + + @get:Rule + val mvrxTestRule = MvRxTestRule() + + private val fakeRoomId = "" + + private val args = LocationLiveMapViewArgs(roomId = fakeRoomId) + + private val getListOfUserLiveLocationUseCase = mockk() + + private fun createViewModel(): LocationLiveMapViewModel { + return LocationLiveMapViewModel( + LocationLiveMapViewState(args), + getListOfUserLiveLocationUseCase + ) + } + + @Test + fun `given the viewModel has been initialized then viewState contains user locations list`() = runTest { + val userLocations = listOf( + UserLiveLocationViewState( + userId = "", + pinDrawable = mockk(), + locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null), + endOfLiveTimestampMillis = 123 + ) + ) + + every { getListOfUserLiveLocationUseCase.execute(fakeRoomId) } returns flowOf(userLocations) + + val viewModel = createViewModel() + viewModel + .test() + .assertState( + LocationLiveMapViewState(args).copy( + userLocations = userLocations + ) + ) + .finish() + } +} From eda0aa97d0a5af2b3149ba991583fe3da28f9beb Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 25 May 2022 14:11:18 +0200 Subject: [PATCH 25/31] Fixing code quality issues --- .../org/matrix/android/sdk/internal/session/room/RoomFactory.kt | 2 +- .../session/room/location/DefaultLocationSharingService.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index 512b88e3e0..ffe7679575 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.room.Room -import org.matrix.android.sdk.internal.session.room.location.DefaultLocationSharingService import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder import org.matrix.android.sdk.internal.session.room.accountdata.DefaultRoomAccountDataService @@ -26,6 +25,7 @@ import org.matrix.android.sdk.internal.session.room.alias.DefaultAliasService import org.matrix.android.sdk.internal.session.room.call.DefaultRoomCallService import org.matrix.android.sdk.internal.session.room.crypto.DefaultRoomCryptoService import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService +import org.matrix.android.sdk.internal.session.room.location.DefaultLocationSharingService import org.matrix.android.sdk.internal.session.room.membership.DefaultMembershipService import org.matrix.android.sdk.internal.session.room.notification.DefaultRoomPushRuleService import org.matrix.android.sdk.internal.session.room.read.DefaultReadService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index cecad77f06..8cf6fcdfbf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. From 03a8289a1398b03f164d12b4b3e1d6d5fb5d73ca Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 26 May 2022 15:45:53 +0300 Subject: [PATCH 26/31] Code review fixes. --- .../im/vector/app/core/di/FragmentModule.kt | 6 ++ .../live/map/LocationLiveMapAction.kt | 5 +- .../live/map/LocationLiveMapViewFragment.kt | 57 ++++++++----------- .../live/map/LocationLiveMapViewModel.kt | 24 ++++++-- .../live/map/LocationLiveMapViewState.kt | 6 +- 5 files changed, 56 insertions(+), 42 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 3dba8b797b..b08f4b2e9f 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -64,6 +64,7 @@ import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.threads.list.views.ThreadListFragment import im.vector.app.features.location.LocationPreviewFragment import im.vector.app.features.location.LocationSharingFragment +import im.vector.app.features.location.live.map.LocationLiveMapViewFragment import im.vector.app.features.login.LoginCaptchaFragment import im.vector.app.features.login.LoginFragment import im.vector.app.features.login.LoginGenericTextInputFormFragment @@ -1005,4 +1006,9 @@ interface FragmentModule { @IntoMap @FragmentKey(LocationPreviewFragment::class) fun bindLocationPreviewFragment(fragment: LocationPreviewFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LocationLiveMapViewFragment::class) + fun bindLocationLiveMapViewFragment(fragment: LocationLiveMapViewFragment): Fragment } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapAction.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapAction.kt index a31e02611f..16cd3badc6 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapAction.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapAction.kt @@ -18,4 +18,7 @@ package im.vector.app.features.location.live.map import im.vector.app.core.platform.VectorViewModelAction -sealed interface LocationLiveMapAction : VectorViewModelAction +sealed class LocationLiveMapAction : VectorViewModelAction { + data class AddMapSymbol(val key: String, val value: Long) : LocationLiveMapAction() + data class RemoveMapSymbol(val key: String) : LocationLiveMapAction() +} diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt index 46c238ad12..9ade47e321 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt @@ -17,13 +17,10 @@ package im.vector.app.features.location.live.map import android.graphics.drawable.Drawable -import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.core.graphics.drawable.toBitmap import androidx.lifecycle.lifecycleScope -import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.mapbox.mapboxsdk.geometry.LatLng @@ -36,7 +33,6 @@ import com.mapbox.mapboxsdk.maps.SupportMapFragment import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions import com.mapbox.mapboxsdk.style.layers.Property -import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.addChildFragment import im.vector.app.core.platform.VectorBaseFragment @@ -44,6 +40,7 @@ import im.vector.app.databinding.FragmentSimpleContainerBinding import im.vector.app.features.location.UrlMapProvider import im.vector.app.features.location.zoomToBounds import im.vector.app.features.location.zoomToLocation +import kotlinx.coroutines.launch import timber.log.Timber import java.lang.ref.WeakReference import javax.inject.Inject @@ -51,13 +48,9 @@ import javax.inject.Inject /** * Screen showing a map with all the current users sharing their live location in a room. */ -@AndroidEntryPoint -class LocationLiveMapViewFragment : VectorBaseFragment() { - - @Inject - lateinit var urlMapProvider: UrlMapProvider - - private val args: LocationLiveMapViewArgs by args() +class LocationLiveMapViewFragment @Inject constructor( + private var urlMapProvider: UrlMapProvider, +) : VectorBaseFragment() { private val viewModel: LocationLiveMapViewModel by fragmentViewModel() @@ -71,16 +64,15 @@ class LocationLiveMapViewFragment : VectorBaseFragment - lifecycleScope.launchWhenCreated { + lifecycleScope.launch { mapboxMap.setStyle(urlMapProvider.getMapUrl()) { style -> mapStyle = style this@LocationLiveMapViewFragment.mapboxMap = WeakReference(mapboxMap) @@ -108,10 +100,8 @@ class LocationLiveMapViewFragment : VectorBaseFragment) { symbolManager?.let { sManager -> val latLngBoundsBuilder = LatLngBounds.Builder() - userLiveLocations.forEach { userLocation -> createOrUpdateSymbol(userLocation, sManager) - if (isMapFirstUpdate) { val latLng = LatLng(userLocation.locationData.latitude, userLocation.locationData.longitude) latLngBoundsBuilder.include(latLng) @@ -123,10 +113,10 @@ class LocationLiveMapViewFragment : VectorBaseFragment + val symbolId = state.mapSymbolIds[userLocation.userId] - if (symbolId == null) { + if (symbolId == null || symbolManager.annotations.get(symbolId) == null) { createSymbol(userLocation, symbolManager) } else { updateSymbol(symbolId, userLocation, symbolManager) @@ -137,7 +127,7 @@ class LocationLiveMapViewFragment : VectorBaseFragment, symbolManager: SymbolManager) { - val userIdsToRemove = viewModel.mapSymbolIds.keys.subtract(userLiveLocations.map { it.userId }.toSet()) - userIdsToRemove - .mapNotNull { userId -> - removeUserPinFromMapStyle(userId) - viewModel.mapSymbolIds[userId] - viewModel.mapSymbolIds.remove(userId) - } - .forEach { symbolId -> - Timber.d("trying to delete symbol with id: $symbolId") - symbolManager.annotations.get(symbolId)?.let { - symbolManager.delete(it) - } + private fun removeOutdatedSymbols(userLiveLocations: List, symbolManager: SymbolManager) = withState(viewModel) { state -> + val userIdsToRemove = state.mapSymbolIds.keys.subtract(userLiveLocations.map { it.userId }.toSet()) + userIdsToRemove.forEach { userId -> + removeUserPinFromMapStyle(userId) + viewModel.handle(LocationLiveMapAction.RemoveMapSymbol(userId)) + + state.mapSymbolIds[userId]?.let { symbolId -> + Timber.d("trying to delete symbol with id: $symbolId") + symbolManager.annotations.get(symbolId)?.let { + symbolManager.delete(it) } + } + } } private fun updateMapZoomWhenNeeded(userLiveLocations: List, latLngBoundsBuilder: LatLngBounds.Builder) { diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt index 8c5f292f25..b14feea667 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt @@ -38,11 +38,6 @@ class LocationLiveMapViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() - /** - * Map to keep track of symbol ids associated to each user Id. - */ - val mapSymbolIds = mutableMapOf() - init { getListOfUserLiveLocationUseCase.execute(initialState.roomId) .onEach { setState { copy(userLocations = it) } } @@ -50,6 +45,23 @@ class LocationLiveMapViewModel @AssistedInject constructor( } override fun handle(action: LocationLiveMapAction) { - // do nothing, no action for now + when (action) { + is LocationLiveMapAction.AddMapSymbol -> handleAddMapSymbol(action) + is LocationLiveMapAction.RemoveMapSymbol -> handleRemoveMapSymbol(action) + } + } + + private fun handleAddMapSymbol(action: LocationLiveMapAction.AddMapSymbol) = withState { state -> + val newMapSymbolIds = state.mapSymbolIds.toMutableMap().apply { set(action.key, action.value) } + setState { + copy(mapSymbolIds = newMapSymbolIds) + } + } + + private fun handleRemoveMapSymbol(action: LocationLiveMapAction.RemoveMapSymbol) = withState { state -> + val newMapSymbolIds = state.mapSymbolIds.toMutableMap().apply { remove(action.key) } + setState { + copy(mapSymbolIds = newMapSymbolIds) + } } } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt index 9fa8635d82..6f21f71e80 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt @@ -22,7 +22,11 @@ import im.vector.app.features.location.LocationData data class LocationLiveMapViewState( val roomId: String, - val userLocations: List = emptyList() + val userLocations: List = emptyList(), + /** + * Map to keep track of symbol ids associated to each user Id. + */ + val mapSymbolIds: Map = emptyMap() ) : MavericksState { constructor(locationLiveMapViewArgs: LocationLiveMapViewArgs) : this( roomId = locationLiveMapViewArgs.roomId From f707f177d923623b7afa0f4201756614e29224e9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 30 May 2022 10:03:27 +0200 Subject: [PATCH 27/31] Improving mapper tests --- ...ocationShareAggregatedSummaryMapperTest.kt | 60 +++++++------------ 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt index f8b3b39350..47d5f46525 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt @@ -16,57 +16,41 @@ package org.matrix.android.sdk.internal.database.mapper -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.unmockkObject -import org.amshove.kluent.internal.assertEquals -import org.junit.After -import org.junit.Before +import com.squareup.moshi.Moshi +import org.amshove.kluent.shouldBeEqualTo import org.junit.Test -import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary +import org.matrix.android.sdk.api.session.room.model.message.LocationInfo +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +private const val ANY_USER_ID = "a-user-id" +private const val ANY_ACTIVE_STATE = true +private const val ANY_TIMEOUT = 123L +private val A_LOCATION_INFO = LocationInfo("a-geo-uri") + class LiveLocationShareAggregatedSummaryMapperTest { private val mapper = LiveLocationShareAggregatedSummaryMapper() - @Before - fun setUp() { - mockkObject(ContentMapper) - } - - @After - fun tearDown() { - unmockkObject(ContentMapper) - } - @Test fun `given an entity then result should be mapped correctly`() { - val userId = "userId" - val timeout = 123L - val isActive = true - val lastKnownLocationContent = "lastKnownLocationContent" - val entity = LiveLocationShareAggregatedSummaryEntity( - userId = userId, - isActive = isActive, - endOfLiveTimestampMillis = timeout, - lastLocationContent = lastKnownLocationContent - ) - val content = mockk() - every { ContentMapper.map(lastKnownLocationContent) } returns content + val entity = anEntity(content = MessageBeaconLocationDataContent(locationInfo = A_LOCATION_INFO)) val summary = mapper.map(entity) - // note: unfortunately the implementation relies on an inline method to map the lastLocationDataContent - // since inline methods do not produce bytecode, it is not mockable and the verification on this field cannot be done - val expectedSummary = LiveLocationShareAggregatedSummary( - userId = userId, - isActive = isActive, - endOfLiveTimestampMillis = timeout, - lastLocationDataContent = null + summary shouldBeEqualTo LiveLocationShareAggregatedSummary( + userId = ANY_USER_ID, + isActive = ANY_ACTIVE_STATE, + endOfLiveTimestampMillis = ANY_TIMEOUT, + lastLocationDataContent = MessageBeaconLocationDataContent(locationInfo = A_LOCATION_INFO) ) - assertEquals(expectedSummary, summary) } + + private fun anEntity(content: MessageBeaconLocationDataContent) = LiveLocationShareAggregatedSummaryEntity( + userId = ANY_USER_ID, + isActive = ANY_ACTIVE_STATE, + endOfLiveTimestampMillis = ANY_TIMEOUT, + lastLocationContent = Moshi.Builder().build().adapter(MessageBeaconLocationDataContent::class.java).toJson(content) + ) } From 066c540eb70bef15b3fb59fcc72ea283117e0934 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 30 May 2022 10:06:04 +0200 Subject: [PATCH 28/31] Filter event id in the DB query for active lives --- .../query/LiveLocationShareAggregatedSummaryEntityQuery.kt | 2 ++ .../livelocation/LiveLocationAggregationProcessor.kt | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt index 67d481b48a..0cc41413f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt @@ -74,11 +74,13 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveIn realm: Realm, roomId: String, userId: String, + ignoredEventId: String ): List { return LiveLocationShareAggregatedSummaryEntity .whereRoomId(realm, roomId = roomId) .equalTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, userId) .equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) + .notEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, ignoredEventId) .findAll() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt index 0317de682a..8f4682a9d5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt @@ -147,9 +147,9 @@ internal class LiveLocationAggregationProcessor @Inject constructor( .findActiveLiveInRoomForUser( realm = realm, roomId = roomId, - userId = userId + userId = userId, + ignoredEventId = currentEventId ) - .filterNot { it.eventId == currentEventId } .forEach { it.isActive = false } } From 7f2279c8a89c64294b495fbf32ff6edb6d8b2b81 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 30 May 2022 10:37:58 +0200 Subject: [PATCH 29/31] Improving view state mapper --- .../UserLiveLocationViewStateMapperTest.kt | 65 ++++++++----------- 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt b/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt index 7f83f99b98..cd20e0f12e 100644 --- a/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt @@ -18,37 +18,29 @@ package im.vector.app.features.location.live.map import android.graphics.drawable.Drawable import im.vector.app.features.location.LocationData -import im.vector.app.features.location.toLocationData import im.vector.app.test.fakes.FakeLocationPinProvider -import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.unmockkStatic import kotlinx.coroutines.test.runTest -import org.amshove.kluent.internal.assertEquals -import org.junit.After -import org.junit.Before +import org.amshove.kluent.shouldBeEqualTo 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.LocationInfo import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent +private const val A_USER_ID = "aUserId" +private const val A_IS_ACTIVE = true +private const val A_END_OF_LIVE_TIMESTAMP = 123L +private const val A_LATITUDE = 40.05 +private const val A_LONGITUDE = 29.24 +private const val A_UNCERTAINTY = 30.0 +private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;$A_UNCERTAINTY" + class UserLiveLocationViewStateMapperTest { private val locationPinProvider = FakeLocationPinProvider() private val userLiveLocationViewStateMapper = UserLiveLocationViewStateMapper(locationPinProvider.instance) - @Before - fun setUp() { - mockkStatic("im.vector.app.features.location.LocationDataKt") - } - - @After - fun tearDown() { - unmockkStatic("im.vector.app.features.location.LocationDataKt") - } - @Test fun `given a summary with invalid data then result is null`() = runTest { val summary1 = LiveLocationShareAggregatedSummary( @@ -58,49 +50,44 @@ class UserLiveLocationViewStateMapperTest { lastLocationDataContent = null, ) val summary2 = summary1.copy(userId = "") - val summaryWithoutLocation = summary1.copy(userId = "userId") + val summaryWithoutLocation = summary1.copy(userId = A_USER_ID) val viewState1 = userLiveLocationViewStateMapper.map(summary1) val viewState2 = userLiveLocationViewStateMapper.map(summary2) val viewState3 = userLiveLocationViewStateMapper.map(summaryWithoutLocation) - assertEquals(null, viewState1) - assertEquals(null, viewState2) - assertEquals(null, viewState3) + viewState1 shouldBeEqualTo null + viewState2 shouldBeEqualTo null + viewState3 shouldBeEqualTo null } @Test fun `given a summary with valid data then result is correctly mapped`() = runTest { - val geoUri = "geoUri" - val userId = "userId" val pinDrawable = mockk() - val endOfLiveTimestampMillis = 123L val locationDataContent = MessageBeaconLocationDataContent( - locationInfo = LocationInfo(geoUri = geoUri) + locationInfo = LocationInfo(geoUri = A_GEO_URI) ) val summary = LiveLocationShareAggregatedSummary( - userId = userId, - isActive = true, - endOfLiveTimestampMillis = endOfLiveTimestampMillis, + userId = A_USER_ID, + isActive = A_IS_ACTIVE, + endOfLiveTimestampMillis = A_END_OF_LIVE_TIMESTAMP, lastLocationDataContent = locationDataContent, ) - val locationData = LocationData( - latitude = 1.0, - longitude = 2.0, - uncertainty = null - ) - every { geoUri.toLocationData() } returns locationData - locationPinProvider.givenCreateForUserId(userId, pinDrawable) + locationPinProvider.givenCreateForUserId(A_USER_ID, pinDrawable) val viewState = userLiveLocationViewStateMapper.map(summary) val expectedViewState = UserLiveLocationViewState( - userId = userId, + userId = A_USER_ID, pinDrawable = pinDrawable, - locationData = locationData, - endOfLiveTimestampMillis = endOfLiveTimestampMillis + locationData = LocationData( + latitude = A_LATITUDE, + longitude = A_LONGITUDE, + uncertainty = A_UNCERTAINTY + ), + endOfLiveTimestampMillis = A_END_OF_LIVE_TIMESTAMP ) - assertEquals(expectedViewState, viewState) + viewState shouldBeEqualTo expectedViewState } } From 1756fa26e100466592cb2373b1defc5753b42065 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 30 May 2022 11:21:23 +0200 Subject: [PATCH 30/31] Using @AndroidEntryPoint for map fragment --- .../src/main/java/im/vector/app/core/di/FragmentModule.kt | 6 ------ .../location/live/map/LocationLiveMapViewFragment.kt | 5 ++++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index b08f4b2e9f..3dba8b797b 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -64,7 +64,6 @@ import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.threads.list.views.ThreadListFragment import im.vector.app.features.location.LocationPreviewFragment import im.vector.app.features.location.LocationSharingFragment -import im.vector.app.features.location.live.map.LocationLiveMapViewFragment import im.vector.app.features.login.LoginCaptchaFragment import im.vector.app.features.login.LoginFragment import im.vector.app.features.login.LoginGenericTextInputFormFragment @@ -1006,9 +1005,4 @@ interface FragmentModule { @IntoMap @FragmentKey(LocationPreviewFragment::class) fun bindLocationPreviewFragment(fragment: LocationPreviewFragment): Fragment - - @Binds - @IntoMap - @FragmentKey(LocationLiveMapViewFragment::class) - fun bindLocationLiveMapViewFragment(fragment: LocationLiveMapViewFragment): Fragment } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt index 9ade47e321..076e5027e6 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt @@ -33,6 +33,7 @@ import com.mapbox.mapboxsdk.maps.SupportMapFragment import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions import com.mapbox.mapboxsdk.style.layers.Property +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.addChildFragment import im.vector.app.core.platform.VectorBaseFragment @@ -48,10 +49,12 @@ import javax.inject.Inject /** * Screen showing a map with all the current users sharing their live location in a room. */ +@AndroidEntryPoint class LocationLiveMapViewFragment @Inject constructor( - private var urlMapProvider: UrlMapProvider, ) : VectorBaseFragment() { + @Inject lateinit var urlMapProvider: UrlMapProvider + private val viewModel: LocationLiveMapViewModel by fragmentViewModel() private var mapboxMap: WeakReference? = null From 538c09991303f14a3eb81ff83e5f2073cb65fbaf Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 30 May 2022 11:44:03 +0200 Subject: [PATCH 31/31] Fixing non necessary breaking line --- .../features/location/live/map/LocationLiveMapViewFragment.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt index 076e5027e6..946b6234c1 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt @@ -50,8 +50,7 @@ import javax.inject.Inject * Screen showing a map with all the current users sharing their live location in a room. */ @AndroidEntryPoint -class LocationLiveMapViewFragment @Inject constructor( -) : VectorBaseFragment() { +class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment() { @Inject lateinit var urlMapProvider: UrlMapProvider