From 01879e252d2c0a4a552d21375060a9d207aef336 Mon Sep 17 00:00:00 2001 From: Maxime Naturel Date: Fri, 4 Mar 2022 18:06:24 +0100 Subject: [PATCH] Comparing target and user location using Flow in ViewModel --- .../location/LocationSharingAction.kt | 1 + .../location/LocationSharingFragment.kt | 24 +++++++---- .../location/LocationSharingViewModel.kt | 38 ++++++++++++++++- .../location/LocationSharingViewState.kt | 2 + .../location/LocationTargetChangeListener.kt | 21 ++++++++++ .../app/features/location/MapTilerMapView.kt | 38 ++++++++++++----- .../domain/usecase/CompareLocationsUseCase.kt | 41 +++++++++++++++++++ 7 files changed, 145 insertions(+), 20 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/location/LocationTargetChangeListener.kt create mode 100644 vector/src/main/java/im/vector/app/features/location/domain/usecase/CompareLocationsUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt index a5adf4d2af..3da5641c84 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt @@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class LocationSharingAction : VectorViewModelAction { object CurrentUserLocationSharingAction : LocationSharingAction() data class PinnedLocationSharingAction(val locationData: LocationData?) : LocationSharingAction() + data class LocationTargetChangeAction(val locationData: LocationData) : LocationSharingAction() } 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 57550d930d..a6b73d7f36 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 @@ -45,7 +45,7 @@ class LocationSharingFragment @Inject constructor( private val urlMapProvider: UrlMapProvider, private val avatarRenderer: AvatarRenderer, private val matrixItemColorProvider: MatrixItemColorProvider -) : VectorBaseFragment() { +) : VectorBaseFragment(), LocationTargetChangeListener { private val viewModel: LocationSharingViewModel by fragmentViewModel() @@ -66,7 +66,10 @@ class LocationSharingFragment @Inject constructor( views.mapView.onCreate(savedInstanceState) lifecycleScope.launchWhenCreated { - views.mapView.initialize(urlMapProvider.getMapUrl()) + views.mapView.initialize( + url = urlMapProvider.getMapUrl(), + locationTargetChangeListener = this@LocationSharingFragment + ) } initOptionsPicker() @@ -115,6 +118,10 @@ class LocationSharingFragment @Inject constructor( super.onDestroy() } + override fun onLocationTargetChange(target: LocationData) { + viewModel.handle(LocationSharingAction.LocationTargetChangeAction(target)) + } + override fun invalidate() = withState(viewModel) { state -> updateMap(state) updateUserAvatar(state.userItem) @@ -138,17 +145,18 @@ class LocationSharingFragment @Inject constructor( private fun initOptionsPicker() { // TODO - // move pin creation into the Fragment - // create a useCase to compare pinnedLocation and userLocation + // create a useCase to compare 2 locations + // update options menu dynamically // change the pin dynamically depending on the current chosen location: cf. LocationPinProvider + // move pin creation into the Fragment? => need to ask other's opinions // reset map to user location when clicking on reset icon // need changes in the event sent when this is a pin drop location? // need changes in the parsing of events when receiving pin drop location?: should it be shown with user avatar or with pin? // set no option at start views.shareLocationOptionsPicker.render() views.shareLocationOptionsPicker.optionPinned.debouncedClicks { - val selectedLocation = views.mapView.getLocationOfMapCenter() - viewModel.handle(LocationSharingAction.PinnedLocationSharingAction(selectedLocation)) + val targetLocation = views.mapView.getLocationOfMapCenter() + viewModel.handle(LocationSharingAction.PinnedLocationSharingAction(targetLocation)) } views.shareLocationOptionsPicker.optionUserCurrent.debouncedClicks { viewModel.handle(LocationSharingAction.CurrentUserLocationSharingAction) @@ -160,9 +168,7 @@ class LocationSharingFragment @Inject constructor( private fun updateMap(state: LocationSharingViewState) { // first, update the options view - // TODO compute distance between userLocation and location at center of map - val isUserLocation = true - if (isUserLocation) { + if (state.areTargetAndUserLocationEqual) { // TODO activate USER_LIVE option when implemented views.shareLocationOptionsPicker.render( LocationSharingOption.USER_CURRENT diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt index 5dd826708d..3fafbd0519 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt @@ -25,18 +25,31 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import im.vector.app.features.location.domain.usecase.CompareLocationsUseCase +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.util.toMatrixItem +private const val TARGET_LOCATION_CHANGE_SAMPLING_PERIOD_IN_MS = 100L + class LocationSharingViewModel @AssistedInject constructor( @Assisted private val initialState: LocationSharingViewState, private val locationTracker: LocationTracker, private val locationPinProvider: LocationPinProvider, - private val session: Session + private val session: Session, + private val compareLocationsUseCase: CompareLocationsUseCase ) : VectorViewModel(initialState), LocationTracker.Callback { private val room = session.getRoom(initialState.roomId)!! + private val locationTargetFlow = MutableSharedFlow() + @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { override fun create(initialState: LocationSharingViewState): LocationSharingViewModel @@ -48,6 +61,7 @@ class LocationSharingViewModel @AssistedInject constructor( locationTracker.start(this) setUserItem() createPin() + compareTargetAndUserLocation() } private fun setUserItem() { @@ -64,6 +78,21 @@ class LocationSharingViewModel @AssistedInject constructor( } } + private fun compareTargetAndUserLocation() { + locationTargetFlow + .sample(TARGET_LOCATION_CHANGE_SAMPLING_PERIOD_IN_MS) + .map { compareTargetLocation(it) } + .distinctUntilChanged() + .onEach { setState { copy(areTargetAndUserLocationEqual = it) } } + .launchIn(viewModelScope) + } + + private suspend fun compareTargetLocation(targetLocation: LocationData): Boolean { + return awaitState().lastKnownUserLocation + ?.let { userLocation -> compareLocationsUseCase.execute(userLocation, targetLocation) } + ?: false + } + override fun onCleared() { super.onCleared() locationTracker.stop() @@ -73,6 +102,7 @@ class LocationSharingViewModel @AssistedInject constructor( when (action) { LocationSharingAction.CurrentUserLocationSharingAction -> handleCurrentUserLocationSharingAction() is LocationSharingAction.PinnedLocationSharingAction -> handlePinnedLocationSharingAction(action) + is LocationSharingAction.LocationTargetChangeAction -> handleLocationTargetChangeAction(action) }.exhaustive } @@ -98,6 +128,12 @@ class LocationSharingViewModel @AssistedInject constructor( } } + private fun handleLocationTargetChangeAction(action: LocationSharingAction.LocationTargetChangeAction) { + viewModelScope.launch { + locationTargetFlow.emit(action.locationData) + } + } + override fun onLocationUpdate(locationData: LocationData) { setState { copy(lastKnownUserLocation = locationData) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt index 4b8587c40c..8faa392416 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt @@ -31,6 +31,8 @@ data class LocationSharingViewState( val roomId: String, val mode: LocationSharingMode, val userItem: MatrixItem.UserItem? = null, + // TODO declare as nullable when we cannot compare? + val areTargetAndUserLocationEqual: Boolean = true, val lastKnownUserLocation: LocationData? = null, // TODO move pin drawable creation into the view? val pinDrawable: Drawable? = null diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTargetChangeListener.kt b/vector/src/main/java/im/vector/app/features/location/LocationTargetChangeListener.kt new file mode 100644 index 0000000000..07e3afb399 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationTargetChangeListener.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 + +interface LocationTargetChangeListener { + fun onLocationTargetChange(target: LocationData) +} 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 dc99b3973a..f8762d4670 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 @@ -48,18 +48,36 @@ class MapTilerMapView @JvmOverloads constructor( /** * For location fragments */ - fun initialize(url: String) { + fun initialize(url: String, locationTargetChangeListener: LocationTargetChangeListener? = null) { Timber.d("## Location: initialize") getMapAsync { map -> - map.setStyle(url) { style -> - mapRefs = MapRefs( - map, - SymbolManager(this, map, style), - style - ) - pendingState?.let { render(it) } - pendingState = null - } + initMapStyle(map, url) + notifyLocationOfMapCenter(locationTargetChangeListener) + listenCameraMove(map, locationTargetChangeListener) + } + } + + private fun initMapStyle(map: MapboxMap, url: String) { + map.setStyle(url) { style -> + mapRefs = MapRefs( + map, + SymbolManager(this, map, style), + style + ) + pendingState?.let { render(it) } + pendingState = null + } + } + + private fun listenCameraMove(map: MapboxMap, locationTargetChangeListener: LocationTargetChangeListener?) { + map.addOnCameraMoveListener { + notifyLocationOfMapCenter(locationTargetChangeListener) + } + } + + private fun notifyLocationOfMapCenter(locationTargetChangeListener: LocationTargetChangeListener?) { + getLocationOfMapCenter()?.let { target -> + locationTargetChangeListener?.onLocationTargetChange(target) } } diff --git a/vector/src/main/java/im/vector/app/features/location/domain/usecase/CompareLocationsUseCase.kt b/vector/src/main/java/im/vector/app/features/location/domain/usecase/CompareLocationsUseCase.kt new file mode 100644 index 0000000000..a2a5486923 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/domain/usecase/CompareLocationsUseCase.kt @@ -0,0 +1,41 @@ +/* + * 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.domain.usecase + +import im.vector.app.features.location.LocationData +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject + +/** + * Use case to check if 2 locations can be considered as equal. + */ +class CompareLocationsUseCase @Inject constructor( + private val session: Session +) { + + // TODO unit test + /** + * Compare the 2 given locations. + * @return true when they are really close and could be considered as the same location, false otherwise + */ + suspend fun execute(location1: LocationData, location2: LocationData): Boolean = + withContext(session.coroutineDispatchers.io) { + // TODO implement real comparison + location1 == location2 + } +}