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 {
object CurrentUserLocationSharingAction : 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 avatarRenderer: AvatarRenderer,
private val matrixItemColorProvider: MatrixItemColorProvider
) : VectorBaseFragment<FragmentLocationSharingBinding>() {
) : VectorBaseFragment<FragmentLocationSharingBinding>(), 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

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.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<LocationSharingViewState, LocationSharingAction, LocationSharingViewEvents>(initialState), LocationTracker.Callback {
private val room = session.getRoom(initialState.roomId)!!
private val locationTargetFlow = MutableSharedFlow<LocationData>()
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<LocationSharingViewModel, LocationSharingViewState> {
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)

View file

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

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
*/
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)
}
}

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