mirror of
https://github.com/element-hq/element-android
synced 2024-11-23 01:45:36 +03:00
Render user location pin on the live location sharing map preview
This commit is contained in:
parent
f676a65544
commit
b5af6f5a0f
8 changed files with 202 additions and 62 deletions
|
@ -33,6 +33,7 @@ class CountUpTimer(
|
||||||
|
|
||||||
private val lastTime: AtomicLong = AtomicLong(clock.epochMillis())
|
private val lastTime: AtomicLong = AtomicLong(clock.epochMillis())
|
||||||
private val elapsedTime: AtomicLong = AtomicLong(0)
|
private val elapsedTime: AtomicLong = AtomicLong(0)
|
||||||
|
|
||||||
// To ensure that the regular tick value is an exact multiple of `intervalInMs`
|
// To ensure that the regular tick value is an exact multiple of `intervalInMs`
|
||||||
private val specialRound = SpecialRound(intervalInMs)
|
private val specialRound = SpecialRound(intervalInMs)
|
||||||
|
|
||||||
|
|
|
@ -23,4 +23,5 @@ sealed class LiveLocationMapAction : VectorViewModelAction {
|
||||||
data class RemoveMapSymbol(val key: String) : LiveLocationMapAction()
|
data class RemoveMapSymbol(val key: String) : LiveLocationMapAction()
|
||||||
object StopSharing : LiveLocationMapAction()
|
object StopSharing : LiveLocationMapAction()
|
||||||
object ShowMapLoadingError : LiveLocationMapAction()
|
object ShowMapLoadingError : LiveLocationMapAction()
|
||||||
|
object ZoomToUserLocation : LiveLocationMapAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,10 @@
|
||||||
package im.vector.app.features.location.live.map
|
package im.vector.app.features.location.live.map
|
||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
import im.vector.app.features.location.LocationData
|
||||||
|
|
||||||
sealed interface LiveLocationMapViewEvents : VectorViewEvents {
|
sealed interface LiveLocationMapViewEvents : VectorViewEvents {
|
||||||
data class Error(val error: Throwable) : LiveLocationMapViewEvents
|
data class LiveLocationError(val error: Throwable) : LiveLocationMapViewEvents
|
||||||
|
data class ZoomToUserLocation(val userLocation: LocationData) : LiveLocationMapViewEvents
|
||||||
|
object UserLocationNotAvailableError : LiveLocationMapViewEvents
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,11 +48,17 @@ import im.vector.app.core.extensions.addChildFragment
|
||||||
import im.vector.app.core.extensions.cleanup
|
import im.vector.app.core.extensions.cleanup
|
||||||
import im.vector.app.core.extensions.configureWith
|
import im.vector.app.core.extensions.configureWith
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.app.core.resources.DrawableProvider
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
|
import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
|
||||||
|
import im.vector.app.core.utils.checkPermissions
|
||||||
|
import im.vector.app.core.utils.onPermissionDeniedDialog
|
||||||
import im.vector.app.core.utils.openLocation
|
import im.vector.app.core.utils.openLocation
|
||||||
|
import im.vector.app.core.utils.registerForPermissionsResult
|
||||||
import im.vector.app.databinding.FragmentLiveLocationMapViewBinding
|
import im.vector.app.databinding.FragmentLiveLocationMapViewBinding
|
||||||
import im.vector.app.features.location.LocationData
|
import im.vector.app.features.location.LocationData
|
||||||
import im.vector.app.features.location.UrlMapProvider
|
import im.vector.app.features.location.UrlMapProvider
|
||||||
|
import im.vector.app.features.location.showUserLocationNotAvailableErrorDialog
|
||||||
import im.vector.app.features.location.zoomToBounds
|
import im.vector.app.features.location.zoomToBounds
|
||||||
import im.vector.app.features.location.zoomToLocation
|
import im.vector.app.features.location.zoomToLocation
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -60,6 +66,8 @@ import timber.log.Timber
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val USER_LOCATION_PIN_ID = "user-location-pin-id"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Screen showing a map with all the current users sharing their live location in a room.
|
* Screen showing a map with all the current users sharing their live location in a room.
|
||||||
*/
|
*/
|
||||||
|
@ -70,6 +78,7 @@ class LiveLocationMapViewFragment :
|
||||||
@Inject lateinit var urlMapProvider: UrlMapProvider
|
@Inject lateinit var urlMapProvider: UrlMapProvider
|
||||||
@Inject lateinit var bottomSheetController: LiveLocationBottomSheetController
|
@Inject lateinit var bottomSheetController: LiveLocationBottomSheetController
|
||||||
@Inject lateinit var dimensionConverter: DimensionConverter
|
@Inject lateinit var dimensionConverter: DimensionConverter
|
||||||
|
@Inject lateinit var drawableProvider: DrawableProvider
|
||||||
|
|
||||||
private val viewModel: LiveLocationMapViewModel by fragmentViewModel()
|
private val viewModel: LiveLocationMapViewModel by fragmentViewModel()
|
||||||
|
|
||||||
|
@ -77,7 +86,7 @@ class LiveLocationMapViewFragment :
|
||||||
private var mapView: MapView? = null
|
private var mapView: MapView? = null
|
||||||
private var symbolManager: SymbolManager? = null
|
private var symbolManager: SymbolManager? = null
|
||||||
private var mapStyle: Style? = null
|
private var mapStyle: Style? = null
|
||||||
private val pendingLiveLocations = mutableListOf<UserLiveLocationViewState>()
|
private val userLocationDrawable by lazy { drawableProvider.getDrawable(R.drawable.ic_location_user) }
|
||||||
private var isMapFirstUpdate = true
|
private var isMapFirstUpdate = true
|
||||||
private var onSymbolClickListener: OnSymbolClickListener? = null
|
private var onSymbolClickListener: OnSymbolClickListener? = null
|
||||||
private var mapLoadingErrorListener: MapView.OnDidFailLoadingMapListener? = null
|
private var mapLoadingErrorListener: MapView.OnDidFailLoadingMapListener? = null
|
||||||
|
@ -90,6 +99,7 @@ class LiveLocationMapViewFragment :
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
observeViewEvents()
|
observeViewEvents()
|
||||||
setupMap()
|
setupMap()
|
||||||
|
initLocateButton()
|
||||||
|
|
||||||
views.liveLocationBottomSheetRecyclerView.configureWith(bottomSheetController, hasFixedSize = false, disableItemAnimation = true)
|
views.liveLocationBottomSheetRecyclerView.configureWith(bottomSheetController, hasFixedSize = false, disableItemAnimation = true)
|
||||||
|
|
||||||
|
@ -107,11 +117,23 @@ class LiveLocationMapViewFragment :
|
||||||
private fun observeViewEvents() {
|
private fun observeViewEvents() {
|
||||||
viewModel.observeViewEvents { viewEvent ->
|
viewModel.observeViewEvents { viewEvent ->
|
||||||
when (viewEvent) {
|
when (viewEvent) {
|
||||||
is LiveLocationMapViewEvents.Error -> displayErrorDialog(viewEvent.error)
|
is LiveLocationMapViewEvents.LiveLocationError -> displayErrorDialog(viewEvent.error)
|
||||||
|
is LiveLocationMapViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(viewEvent)
|
||||||
|
LiveLocationMapViewEvents.UserLocationNotAvailableError -> handleUserLocationNotAvailableError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleZoomToUserLocationEvent(event: LiveLocationMapViewEvents.ZoomToUserLocation) {
|
||||||
|
mapboxMap?.get().zoomToLocation(event.userLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUserLocationNotAvailableError() {
|
||||||
|
showUserLocationNotAvailableErrorDialog {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
onSymbolClickListener?.let { symbolManager?.removeClickListener(it) }
|
onSymbolClickListener?.let { symbolManager?.removeClickListener(it) }
|
||||||
symbolManager?.onDestroy()
|
symbolManager?.onDestroy()
|
||||||
|
@ -141,14 +163,33 @@ class LiveLocationMapViewFragment :
|
||||||
true
|
true
|
||||||
}.also { addClickListener(it) }
|
}.also { addClickListener(it) }
|
||||||
}
|
}
|
||||||
pendingLiveLocations
|
// force refresh of the map using the last viewState
|
||||||
.takeUnless { it.isEmpty() }
|
invalidate()
|
||||||
?.let { updateMap(it) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initLocateButton() {
|
||||||
|
views.liveLocationMapLocateButton.setOnClickListener {
|
||||||
|
if (checkPermissions(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, requireActivity(), foregroundLocationResultLauncher)) {
|
||||||
|
zoomToUserLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun zoomToUserLocation() {
|
||||||
|
viewModel.handle(LiveLocationMapAction.ZoomToUserLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val foregroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
|
||||||
|
if (allGranted) {
|
||||||
|
zoomToUserLocation()
|
||||||
|
} else if (deniedPermanently) {
|
||||||
|
activity?.onPermissionDeniedDialog(R.string.denied_permission_generic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun listenMapLoadingError(mapView: MapView) {
|
private fun listenMapLoadingError(mapView: MapView) {
|
||||||
mapLoadingErrorListener = MapView.OnDidFailLoadingMapListener {
|
mapLoadingErrorListener = MapView.OnDidFailLoadingMapListener {
|
||||||
viewModel.handle(LiveLocationMapAction.ShowMapLoadingError)
|
viewModel.handle(LiveLocationMapAction.ShowMapLoadingError)
|
||||||
|
@ -191,10 +232,15 @@ class LiveLocationMapViewFragment :
|
||||||
views.mapPreviewLoadingError.isVisible = true
|
views.mapPreviewLoadingError.isVisible = true
|
||||||
} else {
|
} else {
|
||||||
views.mapPreviewLoadingError.isGone = true
|
views.mapPreviewLoadingError.isGone = true
|
||||||
updateMap(viewState.userLocations)
|
updateMap(userLiveLocations = viewState.userLocations, userLocation = viewState.lastKnownUserLocation)
|
||||||
|
}
|
||||||
|
if (viewState.isLoadingUserLocation) {
|
||||||
|
showLoadingDialog()
|
||||||
|
} else {
|
||||||
|
dismissLoadingDialog()
|
||||||
}
|
}
|
||||||
updateUserListBottomSheet(viewState.userLocations)
|
updateUserListBottomSheet(viewState.userLocations)
|
||||||
updateLocateButton(showLocateButton = viewState.showLocateButton)
|
updateLocateButton(showLocateButton = viewState.showLocateUserButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateUserListBottomSheet(userLocations: List<UserLiveLocationViewState>) {
|
private fun updateUserListBottomSheet(userLocations: List<UserLiveLocationViewState>) {
|
||||||
|
@ -253,7 +299,10 @@ class LiveLocationMapViewFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMap(userLiveLocations: List<UserLiveLocationViewState>) {
|
private fun updateMap(
|
||||||
|
userLiveLocations: List<UserLiveLocationViewState>,
|
||||||
|
userLocation: LocationData?,
|
||||||
|
) {
|
||||||
symbolManager?.let { sManager ->
|
symbolManager?.let { sManager ->
|
||||||
val latLngBoundsBuilder = LatLngBounds.Builder()
|
val latLngBoundsBuilder = LatLngBounds.Builder()
|
||||||
userLiveLocations.forEach { userLocation ->
|
userLiveLocations.forEach { userLocation ->
|
||||||
|
@ -266,28 +315,60 @@ class LiveLocationMapViewFragment :
|
||||||
|
|
||||||
removeOutdatedSymbols(userLiveLocations, sManager)
|
removeOutdatedSymbols(userLiveLocations, sManager)
|
||||||
updateMapZoomWhenNeeded(userLiveLocations, latLngBoundsBuilder)
|
updateMapZoomWhenNeeded(userLiveLocations, latLngBoundsBuilder)
|
||||||
} ?: postponeUpdateOfMap(userLiveLocations)
|
if (userLocation == null) {
|
||||||
}
|
removeUserSymbol(sManager)
|
||||||
|
} else {
|
||||||
private fun createOrUpdateSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) = withState(viewModel) { state ->
|
createOrUpdateUserSymbol(userLocation, sManager)
|
||||||
val symbolId = state.mapSymbolIds[userLocation.matrixItem.id]
|
}
|
||||||
|
|
||||||
if (symbolId == null || symbolManager.annotations.get(symbolId) == null) {
|
|
||||||
createSymbol(userLocation, symbolManager)
|
|
||||||
} else {
|
|
||||||
updateSymbol(symbolId, userLocation, symbolManager)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) {
|
private fun createOrUpdateSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) {
|
||||||
addUserPinToMapStyle(userLocation.matrixItem.id, userLocation.pinDrawable)
|
val pinId = userLocation.matrixItem.id
|
||||||
val symbolOptions = buildSymbolOptions(userLocation)
|
val pinDrawable = userLocation.pinDrawable
|
||||||
val symbol = symbolManager.create(symbolOptions)
|
createOrUpdateSymbol(pinId, pinDrawable, userLocation.locationData, symbolManager)
|
||||||
viewModel.handle(LiveLocationMapAction.AddMapSymbol(userLocation.matrixItem.id, symbol.id))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSymbol(symbolId: Long, userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) {
|
private fun createOrUpdateUserSymbol(locationData: LocationData, symbolManager: SymbolManager) {
|
||||||
val newLocation = LatLng(userLocation.locationData.latitude, userLocation.locationData.longitude)
|
userLocationDrawable?.let { pinDrawable -> createOrUpdateSymbol(USER_LOCATION_PIN_ID, pinDrawable, locationData, symbolManager) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeUserSymbol(symbolManager: SymbolManager) = withState(viewModel) { state ->
|
||||||
|
val pinId = USER_LOCATION_PIN_ID
|
||||||
|
state.mapSymbolIds[pinId]?.let { symbolId ->
|
||||||
|
removeSymbol(pinId, symbolId, symbolManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createOrUpdateSymbol(
|
||||||
|
pinId: String,
|
||||||
|
pinDrawable: Drawable,
|
||||||
|
locationData: LocationData,
|
||||||
|
symbolManager: SymbolManager
|
||||||
|
) = withState(viewModel) { state ->
|
||||||
|
val symbolId = state.mapSymbolIds[pinId]
|
||||||
|
|
||||||
|
if (symbolId == null || symbolManager.annotations.get(symbolId) == null) {
|
||||||
|
createSymbol(pinId, pinDrawable, locationData, symbolManager)
|
||||||
|
} else {
|
||||||
|
updateSymbol(symbolId, locationData, symbolManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSymbol(
|
||||||
|
pinId: String,
|
||||||
|
pinDrawable: Drawable,
|
||||||
|
locationData: LocationData,
|
||||||
|
symbolManager: SymbolManager
|
||||||
|
) {
|
||||||
|
addPinToMapStyle(pinId, pinDrawable)
|
||||||
|
val symbolOptions = buildSymbolOptions(locationData, pinId)
|
||||||
|
val symbol = symbolManager.create(symbolOptions)
|
||||||
|
viewModel.handle(LiveLocationMapAction.AddMapSymbol(pinId, symbol.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSymbol(symbolId: Long, locationData: LocationData, symbolManager: SymbolManager) {
|
||||||
|
val newLocation = LatLng(locationData.latitude, locationData.longitude)
|
||||||
val symbol = symbolManager.annotations.get(symbolId)
|
val symbol = symbolManager.annotations.get(symbolId)
|
||||||
symbol?.let {
|
symbol?.let {
|
||||||
it.latLng = newLocation
|
it.latLng = newLocation
|
||||||
|
@ -296,17 +377,11 @@ class LiveLocationMapViewFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeOutdatedSymbols(userLiveLocations: List<UserLiveLocationViewState>, symbolManager: SymbolManager) = withState(viewModel) { state ->
|
private fun removeOutdatedSymbols(userLiveLocations: List<UserLiveLocationViewState>, symbolManager: SymbolManager) = withState(viewModel) { state ->
|
||||||
val userIdsToRemove = state.mapSymbolIds.keys.subtract(userLiveLocations.map { it.matrixItem.id }.toSet())
|
val pinIdsToKeep = userLiveLocations.map { it.matrixItem.id } + USER_LOCATION_PIN_ID
|
||||||
userIdsToRemove.forEach { userId ->
|
val pinIdsToRemove = state.mapSymbolIds.keys.subtract(pinIdsToKeep.toSet())
|
||||||
removeUserPinFromMapStyle(userId)
|
pinIdsToRemove.forEach { pinId ->
|
||||||
viewModel.handle(LiveLocationMapAction.RemoveMapSymbol(userId))
|
val symbolId = state.mapSymbolIds[pinId]
|
||||||
|
removeSymbol(pinId, symbolId, symbolManager)
|
||||||
state.mapSymbolIds[userId]?.let { symbolId ->
|
|
||||||
Timber.d("trying to delete symbol with id: $symbolId")
|
|
||||||
symbolManager.annotations.get(symbolId)?.let {
|
|
||||||
symbolManager.delete(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,27 +396,35 @@ class LiveLocationMapViewFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun postponeUpdateOfMap(userLiveLocations: List<UserLiveLocationViewState>) {
|
private fun addPinToMapStyle(pinId: String, pinDrawable: Drawable) {
|
||||||
pendingLiveLocations.clear()
|
|
||||||
pendingLiveLocations.addAll(userLiveLocations)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addUserPinToMapStyle(userId: String, userPinDrawable: Drawable) {
|
|
||||||
mapStyle?.let { style ->
|
mapStyle?.let { style ->
|
||||||
if (style.getImage(userId) == null) {
|
if (style.getImage(pinId) == null) {
|
||||||
style.addImage(userId, userPinDrawable.toBitmap())
|
style.addImage(pinId, pinDrawable.toBitmap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeUserPinFromMapStyle(userId: String) {
|
private fun removeSymbol(pinId: String, symbolId: Long?, symbolManager: SymbolManager) {
|
||||||
mapStyle?.removeImage(userId)
|
removeUserPinFromMapStyle(pinId)
|
||||||
|
|
||||||
|
symbolId?.let { id ->
|
||||||
|
Timber.d("trying to delete symbol with id: $id")
|
||||||
|
symbolManager.annotations.get(id)?.let {
|
||||||
|
symbolManager.delete(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.handle(LiveLocationMapAction.RemoveMapSymbol(pinId))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildSymbolOptions(userLiveLocation: UserLiveLocationViewState) =
|
private fun removeUserPinFromMapStyle(pinId: String) {
|
||||||
|
mapStyle?.removeImage(pinId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSymbolOptions(locationData: LocationData, pinId: String) =
|
||||||
SymbolOptions()
|
SymbolOptions()
|
||||||
.withLatLng(LatLng(userLiveLocation.locationData.latitude, userLiveLocation.locationData.longitude))
|
.withLatLng(LatLng(locationData.latitude, locationData.longitude))
|
||||||
.withIconImage(userLiveLocation.matrixItem.id)
|
.withIconImage(pinId)
|
||||||
.withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
|
.withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
|
||||||
|
|
||||||
private fun handleBottomSheetUserSelected(userId: String) = withState(viewModel) { state ->
|
private fun handleBottomSheetUserSelected(userId: String) = withState(viewModel) { state ->
|
||||||
|
|
|
@ -23,8 +23,11 @@ import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.features.location.LocationData
|
||||||
|
import im.vector.app.features.location.LocationTracker
|
||||||
import im.vector.app.features.location.live.StopLiveLocationShareUseCase
|
import im.vector.app.features.location.live.StopLiveLocationShareUseCase
|
||||||
import im.vector.app.features.location.live.tracking.LocationSharingServiceConnection
|
import im.vector.app.features.location.live.tracking.LocationSharingServiceConnection
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -37,7 +40,11 @@ class LiveLocationMapViewModel @AssistedInject constructor(
|
||||||
getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase,
|
getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase,
|
||||||
private val locationSharingServiceConnection: LocationSharingServiceConnection,
|
private val locationSharingServiceConnection: LocationSharingServiceConnection,
|
||||||
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
|
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
|
||||||
) : VectorViewModel<LiveLocationMapViewState, LiveLocationMapAction, LiveLocationMapViewEvents>(initialState), LocationSharingServiceConnection.Callback {
|
private val locationTracker: LocationTracker,
|
||||||
|
) :
|
||||||
|
VectorViewModel<LiveLocationMapViewState, LiveLocationMapAction, LiveLocationMapViewEvents>(initialState),
|
||||||
|
LocationSharingServiceConnection.Callback,
|
||||||
|
LocationTracker.Callback {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory : MavericksAssistedViewModelFactory<LiveLocationMapViewModel, LiveLocationMapViewState> {
|
interface Factory : MavericksAssistedViewModelFactory<LiveLocationMapViewModel, LiveLocationMapViewState> {
|
||||||
|
@ -48,12 +55,37 @@ class LiveLocationMapViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
getListOfUserLiveLocationUseCase.execute(initialState.roomId)
|
getListOfUserLiveLocationUseCase.execute(initialState.roomId)
|
||||||
.onEach { setState { copy(userLocations = it, showLocateButton = it.none { it.matrixItem.id == session.myUserId }) } }
|
.onEach { setState { copy(userLocations = it, showLocateUserButton = it.none { it.matrixItem.id == session.myUserId }) } }
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
locationSharingServiceConnection.bind(this)
|
locationSharingServiceConnection.bind(this)
|
||||||
|
initLocationTracking()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initLocationTracking() {
|
||||||
|
locationTracker.addCallback(this)
|
||||||
|
locationTracker.locations
|
||||||
|
.onEach(::onLocationUpdate)
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLocationUpdate(locationData: LocationData) = withState { state ->
|
||||||
|
val zoomToUserLocation = state.isLoadingUserLocation
|
||||||
|
val showLocateButton = state.showLocateUserButton
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
lastKnownUserLocation = if (showLocateButton) locationData else null,
|
||||||
|
isLoadingUserLocation = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoomToUserLocation) {
|
||||||
|
_viewEvents.post(LiveLocationMapViewEvents.ZoomToUserLocation(locationData))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
locationTracker.removeCallback(this)
|
||||||
locationSharingServiceConnection.unbind(this)
|
locationSharingServiceConnection.unbind(this)
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
@ -64,6 +96,7 @@ class LiveLocationMapViewModel @AssistedInject constructor(
|
||||||
is LiveLocationMapAction.RemoveMapSymbol -> handleRemoveMapSymbol(action)
|
is LiveLocationMapAction.RemoveMapSymbol -> handleRemoveMapSymbol(action)
|
||||||
LiveLocationMapAction.StopSharing -> handleStopSharing()
|
LiveLocationMapAction.StopSharing -> handleStopSharing()
|
||||||
LiveLocationMapAction.ShowMapLoadingError -> handleShowMapLoadingError()
|
LiveLocationMapAction.ShowMapLoadingError -> handleShowMapLoadingError()
|
||||||
|
LiveLocationMapAction.ZoomToUserLocation -> handleZoomToUserLocation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +118,7 @@ class LiveLocationMapViewModel @AssistedInject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = stopLiveLocationShareUseCase.execute(initialState.roomId)
|
val result = stopLiveLocationShareUseCase.execute(initialState.roomId)
|
||||||
if (result is UpdateLiveLocationShareResult.Failure) {
|
if (result is UpdateLiveLocationShareResult.Failure) {
|
||||||
_viewEvents.post(LiveLocationMapViewEvents.Error(result.error))
|
_viewEvents.post(LiveLocationMapViewEvents.LiveLocationError(result.error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,6 +127,19 @@ class LiveLocationMapViewModel @AssistedInject constructor(
|
||||||
setState { copy(loadingMapHasFailed = true) }
|
setState { copy(loadingMapHasFailed = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO add unit tests
|
||||||
|
private fun handleZoomToUserLocation() = withState { state ->
|
||||||
|
if (!state.isLoadingUserLocation) {
|
||||||
|
setState {
|
||||||
|
copy(isLoadingUserLocation = true)
|
||||||
|
}
|
||||||
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
|
locationTracker.start()
|
||||||
|
locationTracker.requestLastKnownLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onLocationServiceRunning(roomIds: Set<String>) {
|
override fun onLocationServiceRunning(roomIds: Set<String>) {
|
||||||
// NOOP
|
// NOOP
|
||||||
}
|
}
|
||||||
|
@ -103,6 +149,10 @@ class LiveLocationMapViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLocationServiceError(error: Throwable) {
|
override fun onLocationServiceError(error: Throwable) {
|
||||||
_viewEvents.post(LiveLocationMapViewEvents.Error(error))
|
_viewEvents.post(LiveLocationMapViewEvents.LiveLocationError(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNoLocationProviderAvailable() {
|
||||||
|
_viewEvents.post(LiveLocationMapViewEvents.UserLocationNotAvailableError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,9 @@ data class LiveLocationMapViewState(
|
||||||
*/
|
*/
|
||||||
val mapSymbolIds: Map<String, Long> = emptyMap(),
|
val mapSymbolIds: Map<String, Long> = emptyMap(),
|
||||||
val loadingMapHasFailed: Boolean = false,
|
val loadingMapHasFailed: Boolean = false,
|
||||||
val showLocateButton: Boolean = false,
|
val showLocateUserButton: Boolean = false,
|
||||||
|
val isLoadingUserLocation: Boolean = false,
|
||||||
|
val lastKnownUserLocation: LocationData? = null,
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
constructor(liveLocationMapViewArgs: LiveLocationMapViewArgs) : this(
|
constructor(liveLocationMapViewArgs: LiveLocationMapViewArgs) : this(
|
||||||
roomId = liveLocationMapViewArgs.roomId
|
roomId = liveLocationMapViewArgs.roomId
|
||||||
|
|
|
@ -94,12 +94,8 @@ class LocationPreviewViewModel @AssistedInject constructor(
|
||||||
_viewEvents.post(LocationPreviewViewEvents.UserLocationNotAvailableError)
|
_viewEvents.post(LocationPreviewViewEvents.UserLocationNotAvailableError)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onLocationUpdate(locationData: LocationData) {
|
private fun onLocationUpdate(locationData: LocationData) = withState { state ->
|
||||||
withState { state ->
|
val zoomToUserLocation = state.isLoadingUserLocation
|
||||||
if (state.isLoadingUserLocation) {
|
|
||||||
_viewEvents.post(LocationPreviewViewEvents.ZoomToUserLocation(locationData))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
|
@ -107,5 +103,9 @@ class LocationPreviewViewModel @AssistedInject constructor(
|
||||||
isLoadingUserLocation = false,
|
isLoadingUserLocation = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (zoomToUserLocation) {
|
||||||
|
_viewEvents.post(LocationPreviewViewEvents.ZoomToUserLocation(locationData))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ data class LocationPreviewViewState(
|
||||||
val lastKnownUserLocation: LocationData? = null,
|
val lastKnownUserLocation: LocationData? = null,
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
constructor(args: LocationSharingArgs): this(
|
constructor(args: LocationSharingArgs) : this(
|
||||||
pinLocationData = args.initialLocationData,
|
pinLocationData = args.initialLocationData,
|
||||||
pinUserId = args.locationOwnerId,
|
pinUserId = args.locationOwnerId,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue