Comparing target and user location using Flow in ViewModel

This commit is contained in:
Maxime Naturel 2022-03-04 18:06:24 +01:00
parent dec075faf3
commit 01879e252d
7 changed files with 145 additions and 20 deletions

View File

@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class LocationSharingAction : VectorViewModelAction { sealed class LocationSharingAction : VectorViewModelAction {
object CurrentUserLocationSharingAction : LocationSharingAction() object CurrentUserLocationSharingAction : LocationSharingAction()
data class PinnedLocationSharingAction(val locationData: LocationData?) : LocationSharingAction() data class PinnedLocationSharingAction(val locationData: LocationData?) : LocationSharingAction()
data class LocationTargetChangeAction(val locationData: LocationData) : LocationSharingAction()
} }

View File

@ -45,7 +45,7 @@ class LocationSharingFragment @Inject constructor(
private val urlMapProvider: UrlMapProvider, private val urlMapProvider: UrlMapProvider,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val matrixItemColorProvider: MatrixItemColorProvider private val matrixItemColorProvider: MatrixItemColorProvider
) : VectorBaseFragment<FragmentLocationSharingBinding>() { ) : VectorBaseFragment<FragmentLocationSharingBinding>(), LocationTargetChangeListener {
private val viewModel: LocationSharingViewModel by fragmentViewModel() private val viewModel: LocationSharingViewModel by fragmentViewModel()
@ -66,7 +66,10 @@ class LocationSharingFragment @Inject constructor(
views.mapView.onCreate(savedInstanceState) views.mapView.onCreate(savedInstanceState)
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
views.mapView.initialize(urlMapProvider.getMapUrl()) views.mapView.initialize(
url = urlMapProvider.getMapUrl(),
locationTargetChangeListener = this@LocationSharingFragment
)
} }
initOptionsPicker() initOptionsPicker()
@ -115,6 +118,10 @@ class LocationSharingFragment @Inject constructor(
super.onDestroy() super.onDestroy()
} }
override fun onLocationTargetChange(target: LocationData) {
viewModel.handle(LocationSharingAction.LocationTargetChangeAction(target))
}
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
updateMap(state) updateMap(state)
updateUserAvatar(state.userItem) updateUserAvatar(state.userItem)
@ -138,17 +145,18 @@ class LocationSharingFragment @Inject constructor(
private fun initOptionsPicker() { private fun initOptionsPicker() {
// TODO // TODO
// move pin creation into the Fragment // create a useCase to compare 2 locations
// create a useCase to compare pinnedLocation and userLocation // update options menu dynamically
// change the pin dynamically depending on the current chosen location: cf. LocationPinProvider // 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 // 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 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? // 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 // set no option at start
views.shareLocationOptionsPicker.render() views.shareLocationOptionsPicker.render()
views.shareLocationOptionsPicker.optionPinned.debouncedClicks { views.shareLocationOptionsPicker.optionPinned.debouncedClicks {
val selectedLocation = views.mapView.getLocationOfMapCenter() val targetLocation = views.mapView.getLocationOfMapCenter()
viewModel.handle(LocationSharingAction.PinnedLocationSharingAction(selectedLocation)) viewModel.handle(LocationSharingAction.PinnedLocationSharingAction(targetLocation))
} }
views.shareLocationOptionsPicker.optionUserCurrent.debouncedClicks { views.shareLocationOptionsPicker.optionUserCurrent.debouncedClicks {
viewModel.handle(LocationSharingAction.CurrentUserLocationSharingAction) viewModel.handle(LocationSharingAction.CurrentUserLocationSharingAction)
@ -160,9 +168,7 @@ class LocationSharingFragment @Inject constructor(
private fun updateMap(state: LocationSharingViewState) { private fun updateMap(state: LocationSharingViewState) {
// first, update the options view // first, update the options view
// TODO compute distance between userLocation and location at center of map if (state.areTargetAndUserLocationEqual) {
val isUserLocation = true
if (isUserLocation) {
// TODO activate USER_LIVE option when implemented // TODO activate USER_LIVE option when implemented
views.shareLocationOptionsPicker.render( views.shareLocationOptionsPicker.render(
LocationSharingOption.USER_CURRENT LocationSharingOption.USER_CURRENT

View File

@ -25,18 +25,31 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider 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.session.Session
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
private const val TARGET_LOCATION_CHANGE_SAMPLING_PERIOD_IN_MS = 100L
class LocationSharingViewModel @AssistedInject constructor( class LocationSharingViewModel @AssistedInject constructor(
@Assisted private val initialState: LocationSharingViewState, @Assisted private val initialState: LocationSharingViewState,
private val locationTracker: LocationTracker, private val locationTracker: LocationTracker,
private val locationPinProvider: LocationPinProvider, private val locationPinProvider: LocationPinProvider,
private val session: Session private val session: Session,
private val compareLocationsUseCase: CompareLocationsUseCase
) : VectorViewModel<LocationSharingViewState, LocationSharingAction, LocationSharingViewEvents>(initialState), LocationTracker.Callback { ) : VectorViewModel<LocationSharingViewState, LocationSharingAction, LocationSharingViewEvents>(initialState), LocationTracker.Callback {
private val room = session.getRoom(initialState.roomId)!! private val room = session.getRoom(initialState.roomId)!!
private val locationTargetFlow = MutableSharedFlow<LocationData>()
@AssistedFactory @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<LocationSharingViewModel, LocationSharingViewState> { interface Factory : MavericksAssistedViewModelFactory<LocationSharingViewModel, LocationSharingViewState> {
override fun create(initialState: LocationSharingViewState): LocationSharingViewModel override fun create(initialState: LocationSharingViewState): LocationSharingViewModel
@ -48,6 +61,7 @@ class LocationSharingViewModel @AssistedInject constructor(
locationTracker.start(this) locationTracker.start(this)
setUserItem() setUserItem()
createPin() createPin()
compareTargetAndUserLocation()
} }
private fun setUserItem() { 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() { override fun onCleared() {
super.onCleared() super.onCleared()
locationTracker.stop() locationTracker.stop()
@ -73,6 +102,7 @@ class LocationSharingViewModel @AssistedInject constructor(
when (action) { when (action) {
LocationSharingAction.CurrentUserLocationSharingAction -> handleCurrentUserLocationSharingAction() LocationSharingAction.CurrentUserLocationSharingAction -> handleCurrentUserLocationSharingAction()
is LocationSharingAction.PinnedLocationSharingAction -> handlePinnedLocationSharingAction(action) is LocationSharingAction.PinnedLocationSharingAction -> handlePinnedLocationSharingAction(action)
is LocationSharingAction.LocationTargetChangeAction -> handleLocationTargetChangeAction(action)
}.exhaustive }.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) { override fun onLocationUpdate(locationData: LocationData) {
setState { setState {
copy(lastKnownUserLocation = locationData) copy(lastKnownUserLocation = locationData)

View File

@ -31,6 +31,8 @@ data class LocationSharingViewState(
val roomId: String, val roomId: String,
val mode: LocationSharingMode, val mode: LocationSharingMode,
val userItem: MatrixItem.UserItem? = null, val userItem: MatrixItem.UserItem? = null,
// TODO declare as nullable when we cannot compare?
val areTargetAndUserLocationEqual: Boolean = true,
val lastKnownUserLocation: LocationData? = null, val lastKnownUserLocation: LocationData? = null,
// TODO move pin drawable creation into the view? // TODO move pin drawable creation into the view?
val pinDrawable: Drawable? = null val pinDrawable: Drawable? = null

View File

@ -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)
}

View File

@ -48,18 +48,36 @@ class MapTilerMapView @JvmOverloads constructor(
/** /**
* For location fragments * For location fragments
*/ */
fun initialize(url: String) { fun initialize(url: String, locationTargetChangeListener: LocationTargetChangeListener? = null) {
Timber.d("## Location: initialize") Timber.d("## Location: initialize")
getMapAsync { map -> getMapAsync { map ->
map.setStyle(url) { style -> initMapStyle(map, url)
mapRefs = MapRefs( notifyLocationOfMapCenter(locationTargetChangeListener)
map, listenCameraMove(map, locationTargetChangeListener)
SymbolManager(this, map, style), }
style }
)
pendingState?.let { render(it) } private fun initMapStyle(map: MapboxMap, url: String) {
pendingState = null 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)
} }
} }

View File

@ -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
}
}