Comparing target and user location using Flow in ViewModel
This commit is contained in:
parent
dec075faf3
commit
01879e252d
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user