Merge pull request #8144 from vector-im/feature/mna/user-location-in-loc-sharing
[Location sharing] Show own location in map views
This commit is contained in:
commit
6bd150d4cd
|
@ -0,0 +1 @@
|
||||||
|
[Location sharing] Show own location in map views
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import im.vector.app.R
|
||||||
|
|
||||||
|
fun Fragment.showUserLocationNotAvailableErrorDialog(onConfirmListener: () -> Unit) {
|
||||||
|
MaterialAlertDialogBuilder(requireActivity())
|
||||||
|
.setTitle(R.string.location_not_available_dialog_title)
|
||||||
|
.setMessage(R.string.location_not_available_dialog_content)
|
||||||
|
.setPositiveButton(R.string.ok) { _, _ ->
|
||||||
|
onConfirmListener()
|
||||||
|
}
|
||||||
|
.setCancelable(false)
|
||||||
|
.show()
|
||||||
|
}
|
|
@ -176,14 +176,7 @@ class LocationSharingFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLocationNotAvailableError() {
|
private fun handleLocationNotAvailableError() {
|
||||||
MaterialAlertDialogBuilder(requireActivity())
|
showUserLocationNotAvailableErrorDialog { locationSharingNavigator.quit() }
|
||||||
.setTitle(R.string.location_not_available_dialog_title)
|
|
||||||
.setMessage(R.string.location_not_available_dialog_content)
|
|
||||||
.setPositiveButton(R.string.ok) { _, _ ->
|
|
||||||
locationSharingNavigator.quit()
|
|
||||||
}
|
|
||||||
.setCancelable(false)
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLiveLocationSharingNotEnoughPermission() {
|
private fun handleLiveLocationSharingNotEnoughPermission() {
|
||||||
|
|
|
@ -47,7 +47,7 @@ data class LocationSharingViewState(
|
||||||
|
|
||||||
fun LocationSharingViewState.toMapState() = MapState(
|
fun LocationSharingViewState.toMapState() = MapState(
|
||||||
zoomOnlyOnce = true,
|
zoomOnlyOnce = true,
|
||||||
userLocationData = lastKnownUserLocation,
|
pinLocationData = lastKnownUserLocation,
|
||||||
pinId = DEFAULT_PIN_ID,
|
pinId = DEFAULT_PIN_ID,
|
||||||
pinDrawable = null,
|
pinDrawable = null,
|
||||||
// show the map pin only when target location and user location are not equal
|
// show the map pin only when target location and user location are not equal
|
||||||
|
|
|
@ -66,6 +66,8 @@ class LocationTracker @Inject constructor(
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
var hasLocationFromGPSProvider = false
|
var hasLocationFromGPSProvider = false
|
||||||
|
|
||||||
|
private var isStarted = false
|
||||||
|
private var isStarting = false
|
||||||
private var firstLocationHandled = false
|
private var firstLocationHandled = false
|
||||||
private val _locations = MutableSharedFlow<Location>(replay = 1)
|
private val _locations = MutableSharedFlow<Location>(replay = 1)
|
||||||
|
|
||||||
|
@ -90,6 +92,8 @@ class LocationTracker @Inject constructor(
|
||||||
|
|
||||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
||||||
fun start() {
|
fun start() {
|
||||||
|
if (!isStarting && !isStarted) {
|
||||||
|
isStarting = true
|
||||||
Timber.d("start()")
|
Timber.d("start()")
|
||||||
|
|
||||||
if (locationManager == null) {
|
if (locationManager == null) {
|
||||||
|
@ -128,6 +132,9 @@ class LocationTracker @Inject constructor(
|
||||||
notifyLocation(latestKnownLocation)
|
notifyLocation(latestKnownLocation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isStarted = true
|
||||||
|
isStarting = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -148,6 +155,8 @@ class LocationTracker @Inject constructor(
|
||||||
callbacks.clear()
|
callbacks.clear()
|
||||||
hasLocationFromGPSProvider = false
|
hasLocationFromGPSProvider = false
|
||||||
hasLocationFromFusedProvider = false
|
hasLocationFromFusedProvider = false
|
||||||
|
isStarting = false
|
||||||
|
isStarted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,9 +21,10 @@ import androidx.annotation.Px
|
||||||
|
|
||||||
data class MapState(
|
data class MapState(
|
||||||
val zoomOnlyOnce: Boolean,
|
val zoomOnlyOnce: Boolean,
|
||||||
val userLocationData: LocationData? = null,
|
val pinLocationData: LocationData? = null,
|
||||||
val pinId: String,
|
val pinId: String,
|
||||||
val pinDrawable: Drawable? = null,
|
val pinDrawable: Drawable? = null,
|
||||||
val showPin: Boolean = true,
|
val showPin: Boolean = true,
|
||||||
@Px val logoMarginBottom: Int = 0
|
val userLocationData: LocationData? = null,
|
||||||
|
@Px val logoMarginBottom: Int = 0,
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.features.location
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.TypedArray
|
import android.content.res.TypedArray
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
@ -38,6 +39,8 @@ import im.vector.app.R
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
private const val USER_PIN_ID = "user-pin-id"
|
||||||
|
|
||||||
class MapTilerMapView @JvmOverloads constructor(
|
class MapTilerMapView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
|
@ -101,9 +104,11 @@ class MapTilerMapView @JvmOverloads constructor(
|
||||||
|
|
||||||
private fun initMapStyle(map: MapboxMap, url: String) {
|
private fun initMapStyle(map: MapboxMap, url: String) {
|
||||||
map.setStyle(url) { style ->
|
map.setStyle(url) { style ->
|
||||||
|
val symbolManager = SymbolManager(this, map, style)
|
||||||
|
symbolManager.iconAllowOverlap = true
|
||||||
mapRefs = MapRefs(
|
mapRefs = MapRefs(
|
||||||
map,
|
map,
|
||||||
SymbolManager(this, map, style),
|
symbolManager,
|
||||||
style
|
style
|
||||||
)
|
)
|
||||||
pendingState?.let { render(it) }
|
pendingState?.let { render(it) }
|
||||||
|
@ -166,30 +171,44 @@ class MapTilerMapView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val pinDrawable = state.pinDrawable ?: userLocationDrawable
|
val pinDrawable = state.pinDrawable ?: userLocationDrawable
|
||||||
pinDrawable?.let { drawable ->
|
addImageToMapStyle(pinDrawable, state.pinId, safeMapRefs)
|
||||||
if (!safeMapRefs.style.isFullyLoaded ||
|
|
||||||
safeMapRefs.style.getImage(state.pinId) == null) {
|
|
||||||
safeMapRefs.style.addImage(state.pinId, drawable.toBitmap())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.userLocationData?.let { locationData ->
|
safeMapRefs.symbolManager.deleteAll()
|
||||||
|
state.pinLocationData?.let { locationData ->
|
||||||
if (!initZoomDone || !state.zoomOnlyOnce) {
|
if (!initZoomDone || !state.zoomOnlyOnce) {
|
||||||
zoomToLocation(locationData)
|
zoomToLocation(locationData)
|
||||||
initZoomDone = true
|
initZoomDone = true
|
||||||
}
|
}
|
||||||
|
|
||||||
safeMapRefs.symbolManager.deleteAll()
|
|
||||||
if (pinDrawable != null && state.showPin) {
|
if (pinDrawable != null && state.showPin) {
|
||||||
safeMapRefs.symbolManager.create(
|
createSymbol(locationData, state.pinId, safeMapRefs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.userLocationData?.let { locationData ->
|
||||||
|
addImageToMapStyle(userLocationDrawable, USER_PIN_ID, safeMapRefs)
|
||||||
|
if (userLocationDrawable != null) {
|
||||||
|
createSymbol(locationData, USER_PIN_ID, safeMapRefs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addImageToMapStyle(image: Drawable?, imageId: String, mapRefs: MapRefs) {
|
||||||
|
image?.let { drawable ->
|
||||||
|
if (!mapRefs.style.isFullyLoaded || mapRefs.style.getImage(imageId) == null) {
|
||||||
|
mapRefs.style.addImage(imageId, drawable.toBitmap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSymbol(locationData: LocationData, imageId: String, mapRefs: MapRefs) {
|
||||||
|
mapRefs.symbolManager.create(
|
||||||
SymbolOptions()
|
SymbolOptions()
|
||||||
.withLatLng(LatLng(locationData.latitude, locationData.longitude))
|
.withLatLng(LatLng(locationData.latitude, locationData.longitude))
|
||||||
.withIconImage(state.pinId)
|
.withIconImage(imageId)
|
||||||
.withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
|
.withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun zoomToLocation(locationData: LocationData) {
|
fun zoomToLocation(locationData: LocationData) {
|
||||||
Timber.d("## Location: zoomToLocation")
|
Timber.d("## Location: zoomToLocation")
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,8 @@ import android.view.ViewGroup
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.marginBottom
|
||||||
|
import androidx.core.view.marginTop
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
|
@ -46,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
|
||||||
|
@ -58,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.
|
||||||
*/
|
*/
|
||||||
|
@ -68,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()
|
||||||
|
|
||||||
|
@ -75,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
|
||||||
|
@ -88,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)
|
||||||
|
|
||||||
|
@ -105,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()
|
||||||
|
@ -139,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)
|
||||||
|
@ -189,9 +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.showLocateUserButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateUserListBottomSheet(userLocations: List<UserLiveLocationViewState>) {
|
private fun updateUserListBottomSheet(userLocations: List<UserLiveLocationViewState>) {
|
||||||
|
@ -236,7 +285,24 @@ class LiveLocationMapViewFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMap(userLiveLocations: List<UserLiveLocationViewState>) {
|
private fun updateLocateButton(showLocateButton: Boolean) {
|
||||||
|
views.liveLocationMapLocateButton.isVisible = showLocateButton
|
||||||
|
adjustCompassButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adjustCompassButton() {
|
||||||
|
val locateButton = views.liveLocationMapLocateButton
|
||||||
|
locateButton.post {
|
||||||
|
val marginTop = locateButton.height + locateButton.marginTop + locateButton.marginBottom
|
||||||
|
val marginRight = locateButton.context.resources.getDimensionPixelOffset(R.dimen.location_sharing_compass_button_margin_horizontal)
|
||||||
|
mapboxMap?.get()?.uiSettings?.setCompassMargins(0, marginTop, marginRight, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
@ -249,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 {
|
||||||
|
createOrUpdateUserSymbol(userLocation, sManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createOrUpdateSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) = withState(viewModel) { state ->
|
private fun createOrUpdateSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) {
|
||||||
val symbolId = state.mapSymbolIds[userLocation.matrixItem.id]
|
val pinId = userLocation.matrixItem.id
|
||||||
|
val pinDrawable = userLocation.pinDrawable
|
||||||
|
createOrUpdateSymbol(pinId, pinDrawable, userLocation.locationData, symbolManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createOrUpdateUserSymbol(locationData: LocationData, symbolManager: SymbolManager) {
|
||||||
|
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) {
|
if (symbolId == null || symbolManager.annotations.get(symbolId) == null) {
|
||||||
createSymbol(userLocation, symbolManager)
|
createSymbol(pinId, pinDrawable, locationData, symbolManager)
|
||||||
} else {
|
} else {
|
||||||
updateSymbol(symbolId, userLocation, symbolManager)
|
updateSymbol(symbolId, locationData, symbolManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) {
|
private fun createSymbol(
|
||||||
addUserPinToMapStyle(userLocation.matrixItem.id, userLocation.pinDrawable)
|
pinId: String,
|
||||||
val symbolOptions = buildSymbolOptions(userLocation)
|
pinDrawable: Drawable,
|
||||||
|
locationData: LocationData,
|
||||||
|
symbolManager: SymbolManager
|
||||||
|
) {
|
||||||
|
addPinToMapStyle(pinId, pinDrawable)
|
||||||
|
val symbolOptions = buildSymbolOptions(locationData, pinId)
|
||||||
val symbol = symbolManager.create(symbolOptions)
|
val symbol = symbolManager.create(symbolOptions)
|
||||||
viewModel.handle(LiveLocationMapAction.AddMapSymbol(userLocation.matrixItem.id, symbol.id))
|
viewModel.handle(LiveLocationMapAction.AddMapSymbol(pinId, symbol.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSymbol(symbolId: Long, userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) {
|
private fun updateSymbol(symbolId: Long, locationData: LocationData, symbolManager: SymbolManager) {
|
||||||
val newLocation = LatLng(userLocation.locationData.latitude, userLocation.locationData.longitude)
|
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
|
||||||
|
@ -279,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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildSymbolOptions(userLiveLocation: UserLiveLocationViewState) =
|
viewModel.handle(LiveLocationMapAction.RemoveMapSymbol(pinId))
|
||||||
|
}
|
||||||
|
|
||||||
|
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,19 +23,27 @@ 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.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
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
|
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
|
||||||
|
|
||||||
class LiveLocationMapViewModel @AssistedInject constructor(
|
class LiveLocationMapViewModel @AssistedInject constructor(
|
||||||
@Assisted private val initialState: LiveLocationMapViewState,
|
@Assisted private val initialState: LiveLocationMapViewState,
|
||||||
|
private val session: Session,
|
||||||
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> {
|
||||||
|
@ -46,12 +54,37 @@ class LiveLocationMapViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
getListOfUserLiveLocationUseCase.execute(initialState.roomId)
|
getListOfUserLiveLocationUseCase.execute(initialState.roomId)
|
||||||
.onEach { setState { copy(userLocations = it) } }
|
.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()
|
||||||
}
|
}
|
||||||
|
@ -62,6 +95,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +117,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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,6 +126,18 @@ class LiveLocationMapViewModel @AssistedInject constructor(
|
||||||
setState { copy(loadingMapHasFailed = true) }
|
setState { copy(loadingMapHasFailed = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleZoomToUserLocation() = withState { state ->
|
||||||
|
if (!state.isLoadingUserLocation) {
|
||||||
|
setState {
|
||||||
|
copy(isLoadingUserLocation = true)
|
||||||
|
}
|
||||||
|
viewModelScope.launch(session.coroutineDispatchers.main) {
|
||||||
|
locationTracker.start()
|
||||||
|
locationTracker.requestLastKnownLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onLocationServiceRunning(roomIds: Set<String>) {
|
override fun onLocationServiceRunning(roomIds: Set<String>) {
|
||||||
// NOOP
|
// NOOP
|
||||||
}
|
}
|
||||||
|
@ -101,6 +147,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,6 +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 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
|
||||||
|
|
|
@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class LocationPreviewAction : VectorViewModelAction {
|
sealed class LocationPreviewAction : VectorViewModelAction {
|
||||||
object ShowMapLoadingError : LocationPreviewAction()
|
object ShowMapLoadingError : LocationPreviewAction()
|
||||||
|
object ZoomToUserLocation : LocationPreviewAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,18 +31,22 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.core.platform.VectorMenuProvider
|
import im.vector.app.core.platform.VectorMenuProvider
|
||||||
|
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.FragmentLocationPreviewBinding
|
import im.vector.app.databinding.FragmentLocationPreviewBinding
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
|
|
||||||
import im.vector.app.features.location.DEFAULT_PIN_ID
|
import im.vector.app.features.location.DEFAULT_PIN_ID
|
||||||
import im.vector.app.features.location.LocationSharingArgs
|
import im.vector.app.features.location.LocationSharingArgs
|
||||||
import im.vector.app.features.location.MapState
|
import im.vector.app.features.location.MapState
|
||||||
import im.vector.app.features.location.UrlMapProvider
|
import im.vector.app.features.location.UrlMapProvider
|
||||||
|
import im.vector.app.features.location.showUserLocationNotAvailableErrorDialog
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* TODO Move locationPinProvider to a ViewModel
|
* Screen displaying the expanded map of a static location share.
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class LocationPreviewFragment :
|
class LocationPreviewFragment :
|
||||||
|
@ -50,7 +54,6 @@ class LocationPreviewFragment :
|
||||||
VectorMenuProvider {
|
VectorMenuProvider {
|
||||||
|
|
||||||
@Inject lateinit var urlMapProvider: UrlMapProvider
|
@Inject lateinit var urlMapProvider: UrlMapProvider
|
||||||
@Inject lateinit var locationPinProvider: LocationPinProvider
|
|
||||||
|
|
||||||
private val args: LocationSharingArgs by args()
|
private val args: LocationSharingArgs by args()
|
||||||
|
|
||||||
|
@ -76,8 +79,29 @@ class LocationPreviewFragment :
|
||||||
|
|
||||||
lifecycleScope.launchWhenCreated {
|
lifecycleScope.launchWhenCreated {
|
||||||
views.mapView.initialize(urlMapProvider.getMapUrl())
|
views.mapView.initialize(urlMapProvider.getMapUrl())
|
||||||
loadPinDrawable()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeViewEvents()
|
||||||
|
initLocateButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeViewEvents() {
|
||||||
|
viewModel.observeViewEvents {
|
||||||
|
when (it) {
|
||||||
|
LocationPreviewViewEvents.UserLocationNotAvailableError -> handleUserLocationNotAvailableError()
|
||||||
|
is LocationPreviewViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUserLocationNotAvailableError() {
|
||||||
|
showUserLocationNotAvailableErrorDialog {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleZoomToUserLocationEvent(event: LocationPreviewViewEvents.ZoomToUserLocation) {
|
||||||
|
views.mapView.zoomToLocation(event.userLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
@ -124,6 +148,24 @@ class LocationPreviewFragment :
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) { state ->
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
views.mapPreviewLoadingError.isVisible = state.loadingMapHasFailed
|
views.mapPreviewLoadingError.isVisible = state.loadingMapHasFailed
|
||||||
|
if (state.isLoadingUserLocation) {
|
||||||
|
showLoadingDialog()
|
||||||
|
} else {
|
||||||
|
dismissLoadingDialog()
|
||||||
|
}
|
||||||
|
updateMap(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMap(viewState: LocationPreviewViewState) {
|
||||||
|
views.mapView.render(
|
||||||
|
MapState(
|
||||||
|
zoomOnlyOnce = true,
|
||||||
|
pinLocationData = viewState.pinLocationData,
|
||||||
|
pinId = viewState.pinUserId ?: DEFAULT_PIN_ID,
|
||||||
|
pinDrawable = viewState.pinDrawable,
|
||||||
|
userLocationData = viewState.lastKnownUserLocation,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMenuRes() = R.menu.menu_location_preview
|
override fun getMenuRes() = R.menu.menu_location_preview
|
||||||
|
@ -143,21 +185,23 @@ class LocationPreviewFragment :
|
||||||
openLocation(requireActivity(), location.latitude, location.longitude)
|
openLocation(requireActivity(), location.latitude, location.longitude)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadPinDrawable() {
|
private fun initLocateButton() {
|
||||||
val location = args.initialLocationData ?: return
|
views.mapView.locateButton.setOnClickListener {
|
||||||
val userId = args.locationOwnerId
|
if (checkPermissions(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, requireActivity(), foregroundLocationResultLauncher)) {
|
||||||
|
zoomToUserLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
locationPinProvider.create(userId) { pinDrawable ->
|
private fun zoomToUserLocation() {
|
||||||
lifecycleScope.launchWhenResumed {
|
viewModel.handle(LocationPreviewAction.ZoomToUserLocation)
|
||||||
views.mapView.render(
|
}
|
||||||
MapState(
|
|
||||||
zoomOnlyOnce = true,
|
private val foregroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
|
||||||
userLocationData = location,
|
if (allGranted) {
|
||||||
pinId = args.locationOwnerId ?: DEFAULT_PIN_ID,
|
zoomToUserLocation()
|
||||||
pinDrawable = pinDrawable
|
} else if (deniedPermanently) {
|
||||||
)
|
activity?.onPermissionDeniedDialog(R.string.denied_permission_generic)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.preview
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
import im.vector.app.features.location.LocationData
|
||||||
|
|
||||||
|
sealed class LocationPreviewViewEvents : VectorViewEvents {
|
||||||
|
data class ZoomToUserLocation(val userLocation: LocationData) : LocationPreviewViewEvents()
|
||||||
|
object UserLocationNotAvailableError : LocationPreviewViewEvents()
|
||||||
|
}
|
|
@ -22,12 +22,21 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
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.EmptyViewEvents
|
|
||||||
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.location.LocationData
|
||||||
|
import im.vector.app.features.location.LocationTracker
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
|
||||||
class LocationPreviewViewModel @AssistedInject constructor(
|
class LocationPreviewViewModel @AssistedInject constructor(
|
||||||
@Assisted private val initialState: LocationPreviewViewState,
|
@Assisted private val initialState: LocationPreviewViewState,
|
||||||
) : VectorViewModel<LocationPreviewViewState, LocationPreviewAction, EmptyViewEvents>(initialState) {
|
private val session: Session,
|
||||||
|
private val locationPinProvider: LocationPinProvider,
|
||||||
|
private val locationTracker: LocationTracker,
|
||||||
|
) : VectorViewModel<LocationPreviewViewState, LocationPreviewAction, LocationPreviewViewEvents>(initialState), LocationTracker.Callback {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory : MavericksAssistedViewModelFactory<LocationPreviewViewModel, LocationPreviewViewState> {
|
interface Factory : MavericksAssistedViewModelFactory<LocationPreviewViewModel, LocationPreviewViewState> {
|
||||||
|
@ -36,13 +45,68 @@ class LocationPreviewViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
companion object : MavericksViewModelFactory<LocationPreviewViewModel, LocationPreviewViewState> by hiltMavericksViewModelFactory()
|
companion object : MavericksViewModelFactory<LocationPreviewViewModel, LocationPreviewViewState> by hiltMavericksViewModelFactory()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initPin(initialState.pinUserId)
|
||||||
|
initLocationTracking()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initPin(userId: String?) {
|
||||||
|
locationPinProvider.create(userId) { pinDrawable ->
|
||||||
|
setState { copy(pinDrawable = pinDrawable) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initLocationTracking() {
|
||||||
|
locationTracker.addCallback(this)
|
||||||
|
locationTracker.locations
|
||||||
|
.onEach(::onLocationUpdate)
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
locationTracker.removeCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun handle(action: LocationPreviewAction) {
|
override fun handle(action: LocationPreviewAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
LocationPreviewAction.ShowMapLoadingError -> handleShowMapLoadingError()
|
LocationPreviewAction.ShowMapLoadingError -> handleShowMapLoadingError()
|
||||||
|
LocationPreviewAction.ZoomToUserLocation -> handleZoomToUserLocationAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShowMapLoadingError() {
|
private fun handleShowMapLoadingError() {
|
||||||
setState { copy(loadingMapHasFailed = true) }
|
setState { copy(loadingMapHasFailed = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleZoomToUserLocationAction() = withState { state ->
|
||||||
|
if (!state.isLoadingUserLocation) {
|
||||||
|
setState {
|
||||||
|
copy(isLoadingUserLocation = true)
|
||||||
|
}
|
||||||
|
viewModelScope.launch(session.coroutineDispatchers.main) {
|
||||||
|
locationTracker.start()
|
||||||
|
locationTracker.requestLastKnownLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNoLocationProviderAvailable() {
|
||||||
|
_viewEvents.post(LocationPreviewViewEvents.UserLocationNotAvailableError)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLocationUpdate(locationData: LocationData) = withState { state ->
|
||||||
|
val zoomToUserLocation = state.isLoadingUserLocation
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
lastKnownUserLocation = locationData,
|
||||||
|
isLoadingUserLocation = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoomToUserLocation) {
|
||||||
|
_viewEvents.post(LocationPreviewViewEvents.ZoomToUserLocation(locationData))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,22 @@
|
||||||
|
|
||||||
package im.vector.app.features.location.preview
|
package im.vector.app.features.location.preview
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
import im.vector.app.features.location.LocationData
|
||||||
|
import im.vector.app.features.location.LocationSharingArgs
|
||||||
|
|
||||||
data class LocationPreviewViewState(
|
data class LocationPreviewViewState(
|
||||||
val loadingMapHasFailed: Boolean = false
|
val pinLocationData: LocationData? = null,
|
||||||
) : MavericksState
|
val pinUserId: String? = null,
|
||||||
|
val pinDrawable: Drawable? = null,
|
||||||
|
val loadingMapHasFailed: Boolean = false,
|
||||||
|
val isLoadingUserLocation: Boolean = false,
|
||||||
|
val lastKnownUserLocation: LocationData? = null,
|
||||||
|
) : MavericksState {
|
||||||
|
|
||||||
|
constructor(args: LocationSharingArgs) : this(
|
||||||
|
pinLocationData = args.initialLocationData,
|
||||||
|
pinUserId = args.locationOwnerId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
android:width="20dp"
|
||||||
<item>
|
android:height="28dp"
|
||||||
<shape android:shape="oval">
|
android:viewportWidth="20"
|
||||||
<size
|
android:viewportHeight="28">
|
||||||
android:width="13dp"
|
<path
|
||||||
android:height="13dp" />
|
android:pathData="M10,0.667C4.84,0.667 0.667,4.953 0.667,10.254C0.667,15.965 6.56,23.841 8.987,26.84C9.52,27.497 10.493,27.497 11.027,26.84C13.44,23.841 19.333,15.965 19.333,10.254C19.333,4.953 15.16,0.667 10,0.667ZM10,13.678C8.16,13.678 6.667,12.144 6.667,10.254C6.667,8.364 8.16,6.83 10,6.83C11.84,6.83 13.333,8.364 13.333,10.254C13.333,12.144 11.84,13.678 10,13.678Z"
|
||||||
<solid android:color="?colorPrimary" />
|
android:fillColor="#0DBD8B"/>
|
||||||
<stroke
|
</vector>
|
||||||
android:width="2dp"
|
|
||||||
android:color="@color/palette_white" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
</layer-list>
|
|
||||||
|
|
|
@ -17,6 +17,20 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/liveLocationMapLocateButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|end"
|
||||||
|
android:layout_marginHorizontal="@dimen/location_sharing_locate_button_margin_horizontal"
|
||||||
|
android:layout_marginVertical="@dimen/location_sharing_locate_button_margin_vertical"
|
||||||
|
android:clickable="true"
|
||||||
|
android:contentDescription="@string/a11y_location_share_locate_button"
|
||||||
|
android:focusable="true"
|
||||||
|
android:src="@drawable/btn_locate"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<im.vector.app.features.location.MapLoadingErrorView
|
<im.vector.app.features.location.MapLoadingErrorView
|
||||||
android:id="@+id/mapPreviewLoadingError"
|
android:id="@+id/mapPreviewLoadingError"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:mapbox_renderTextureMode="true"
|
app:mapbox_renderTextureMode="true"
|
||||||
app:showLocateButton="false" />
|
app:showLocateButton="true" />
|
||||||
|
|
||||||
<im.vector.app.features.location.MapLoadingErrorView
|
<im.vector.app.features.location.MapLoadingErrorView
|
||||||
android:id="@+id/mapPreviewLoadingError"
|
android:id="@+id/mapPreviewLoadingError"
|
||||||
|
|
|
@ -20,13 +20,14 @@ import com.airbnb.mvrx.test.MavericksTestRule
|
||||||
import im.vector.app.features.location.LocationData
|
import im.vector.app.features.location.LocationData
|
||||||
import im.vector.app.features.location.live.StopLiveLocationShareUseCase
|
import im.vector.app.features.location.live.StopLiveLocationShareUseCase
|
||||||
import im.vector.app.test.fakes.FakeLocationSharingServiceConnection
|
import im.vector.app.test.fakes.FakeLocationSharingServiceConnection
|
||||||
|
import im.vector.app.test.fakes.FakeLocationTracker
|
||||||
|
import im.vector.app.test.fakes.FakeSession
|
||||||
import im.vector.app.test.test
|
import im.vector.app.test.test
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.unmockkAll
|
import io.mockk.unmockkAll
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -41,16 +42,20 @@ class LiveLocationMapViewModelTest {
|
||||||
|
|
||||||
private val args = LiveLocationMapViewArgs(roomId = A_ROOM_ID)
|
private val args = LiveLocationMapViewArgs(roomId = A_ROOM_ID)
|
||||||
|
|
||||||
private val getListOfUserLiveLocationUseCase = mockk<GetListOfUserLiveLocationUseCase>()
|
private val fakeSession = FakeSession()
|
||||||
private val locationServiceConnection = FakeLocationSharingServiceConnection()
|
private val fakeGetListOfUserLiveLocationUseCase = mockk<GetListOfUserLiveLocationUseCase>()
|
||||||
private val stopLiveLocationShareUseCase = mockk<StopLiveLocationShareUseCase>()
|
private val fakeLocationSharingServiceConnection = FakeLocationSharingServiceConnection()
|
||||||
|
private val fakeStopLiveLocationShareUseCase = mockk<StopLiveLocationShareUseCase>()
|
||||||
|
private val fakeLocationTracker = FakeLocationTracker()
|
||||||
|
|
||||||
private fun createViewModel(): LiveLocationMapViewModel {
|
private fun createViewModel(): LiveLocationMapViewModel {
|
||||||
return LiveLocationMapViewModel(
|
return LiveLocationMapViewModel(
|
||||||
LiveLocationMapViewState(args),
|
LiveLocationMapViewState(args),
|
||||||
getListOfUserLiveLocationUseCase,
|
session = fakeSession,
|
||||||
locationServiceConnection.instance,
|
getListOfUserLiveLocationUseCase = fakeGetListOfUserLiveLocationUseCase,
|
||||||
stopLiveLocationShareUseCase
|
locationSharingServiceConnection = fakeLocationSharingServiceConnection.instance,
|
||||||
|
stopLiveLocationShareUseCase = fakeStopLiveLocationShareUseCase,
|
||||||
|
locationTracker = fakeLocationTracker.instance,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,30 +65,94 @@ class LiveLocationMapViewModelTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given the viewModel has been initialized then viewState contains user locations list`() = runTest {
|
fun `given the viewModel has been initialized then viewState contains user locations list and location tracker is setup`() {
|
||||||
val userLocations = listOf(
|
// Given
|
||||||
UserLiveLocationViewState(
|
val userLocations = listOf(givenAUserLiveLocationViewState(userId = "@userId1:matrix.org"))
|
||||||
MatrixItem.UserItem(id = "@userId1:matrix.org", displayName = "User 1", avatarUrl = ""),
|
fakeLocationSharingServiceConnection.givenBind()
|
||||||
|
every { fakeGetListOfUserLiveLocationUseCase.execute(A_ROOM_ID) } returns flowOf(userLocations)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest
|
||||||
|
.assertState(
|
||||||
|
LiveLocationMapViewState(args).copy(
|
||||||
|
userLocations = userLocations,
|
||||||
|
showLocateUserButton = true,
|
||||||
|
)
|
||||||
|
).finish()
|
||||||
|
fakeLocationSharingServiceConnection.verifyBind(viewModel)
|
||||||
|
fakeLocationTracker.verifyAddCallback(viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given the viewModel when it is cleared cleanUp are done`() {
|
||||||
|
// Given
|
||||||
|
fakeLocationSharingServiceConnection.givenBind()
|
||||||
|
fakeLocationSharingServiceConnection.givenUnbind()
|
||||||
|
every { fakeGetListOfUserLiveLocationUseCase.execute(A_ROOM_ID) } returns flowOf(emptyList())
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
// When
|
||||||
|
viewModel.onCleared()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
fakeLocationSharingServiceConnection.verifyUnbind(viewModel)
|
||||||
|
fakeLocationTracker.verifyRemoveCallback(viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given current user shares their live location then locate button should not be shown`() {
|
||||||
|
// Given
|
||||||
|
val userLocations = listOf(givenAUserLiveLocationViewState(userId = fakeSession.myUserId))
|
||||||
|
fakeLocationSharingServiceConnection.givenBind()
|
||||||
|
every { fakeGetListOfUserLiveLocationUseCase.execute(A_ROOM_ID) } returns flowOf(userLocations)
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest
|
||||||
|
.assertState(
|
||||||
|
LiveLocationMapViewState(args).copy(
|
||||||
|
userLocations = userLocations,
|
||||||
|
showLocateUserButton = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given current user does not share their live location then locate button should be shown`() {
|
||||||
|
// Given
|
||||||
|
val userLocations = listOf(givenAUserLiveLocationViewState(userId = "@userId1:matrix.org"))
|
||||||
|
fakeLocationSharingServiceConnection.givenBind()
|
||||||
|
every { fakeGetListOfUserLiveLocationUseCase.execute(A_ROOM_ID) } returns flowOf(userLocations)
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest
|
||||||
|
.assertState(
|
||||||
|
LiveLocationMapViewState(args).copy(
|
||||||
|
userLocations = userLocations,
|
||||||
|
showLocateUserButton = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenAUserLiveLocationViewState(userId: String) = UserLiveLocationViewState(
|
||||||
|
MatrixItem.UserItem(id = userId, displayName = "User 1", avatarUrl = ""),
|
||||||
pinDrawable = mockk(),
|
pinDrawable = mockk(),
|
||||||
locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null),
|
locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null),
|
||||||
endOfLiveTimestampMillis = 123,
|
endOfLiveTimestampMillis = 123,
|
||||||
locationTimestampMillis = 123,
|
locationTimestampMillis = 123,
|
||||||
showStopSharingButton = false
|
showStopSharingButton = false
|
||||||
)
|
)
|
||||||
)
|
|
||||||
locationServiceConnection.givenBind()
|
|
||||||
every { getListOfUserLiveLocationUseCase.execute(A_ROOM_ID) } returns flowOf(userLocations)
|
|
||||||
|
|
||||||
val viewModel = createViewModel()
|
|
||||||
viewModel
|
|
||||||
.test()
|
|
||||||
.assertState(
|
|
||||||
LiveLocationMapViewState(args).copy(
|
|
||||||
userLocations = userLocations
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.finish()
|
|
||||||
|
|
||||||
locationServiceConnection.verifyBind(viewModel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,8 @@
|
||||||
package im.vector.app.test.fakes
|
package im.vector.app.test.fakes
|
||||||
|
|
||||||
import im.vector.app.features.location.live.tracking.LocationSharingServiceConnection
|
import im.vector.app.features.location.live.tracking.LocationSharingServiceConnection
|
||||||
import io.mockk.every
|
import io.mockk.justRun
|
||||||
import io.mockk.just
|
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.runs
|
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
|
||||||
class FakeLocationSharingServiceConnection {
|
class FakeLocationSharingServiceConnection {
|
||||||
|
@ -28,10 +26,18 @@ class FakeLocationSharingServiceConnection {
|
||||||
val instance = mockk<LocationSharingServiceConnection>()
|
val instance = mockk<LocationSharingServiceConnection>()
|
||||||
|
|
||||||
fun givenBind() {
|
fun givenBind() {
|
||||||
every { instance.bind(any()) } just runs
|
justRun { instance.bind(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun verifyBind(callback: LocationSharingServiceConnection.Callback) {
|
fun verifyBind(callback: LocationSharingServiceConnection.Callback) {
|
||||||
verify { instance.bind(callback) }
|
verify { instance.bind(callback) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenUnbind() {
|
||||||
|
justRun { instance.unbind(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyUnbind(callback: LocationSharingServiceConnection.Callback) {
|
||||||
|
verify { instance.unbind(callback) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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.test.fakes
|
||||||
|
|
||||||
|
import im.vector.app.features.location.LocationTracker
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
|
||||||
|
class FakeLocationTracker {
|
||||||
|
|
||||||
|
val instance: LocationTracker = mockk(relaxed = true)
|
||||||
|
|
||||||
|
fun verifyAddCallback(callback: LocationTracker.Callback) {
|
||||||
|
verify { instance.addCallback(callback) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyRemoveCallback(callback: LocationTracker.Callback) {
|
||||||
|
verify { instance.removeCallback(callback) }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue