From 7242f1c2f14556e221a3a53d21bb6eadf60b51e7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Jan 2022 16:18:40 +0100 Subject: [PATCH 01/22] Fix a crash when disabling the location on the device `LocationListener` does not have default implementation for some methods for Android versions below R --- .../java/im/vector/app/features/location/LocationTracker.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 0c0315cf34..8495d826d9 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -19,16 +19,16 @@ package im.vector.app.features.location import android.Manifest import android.content.Context import android.location.Location -import android.location.LocationListener import android.location.LocationManager import androidx.annotation.RequiresPermission import androidx.core.content.getSystemService +import androidx.core.location.LocationListenerCompat import timber.log.Timber import javax.inject.Inject class LocationTracker @Inject constructor( private val context: Context -) : LocationListener { +) : LocationListenerCompat { interface Callback { fun onLocationUpdate(locationData: LocationData) From db3f60b4b1ded40ff3de01e56794d73a12c72901 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Jan 2022 16:23:39 +0100 Subject: [PATCH 02/22] Inform the user when the location provider is disabled. --- .../java/im/vector/app/features/location/LocationTracker.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 8495d826d9..9ccf79de33 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -83,6 +83,10 @@ class LocationTracker @Inject constructor( callback?.onLocationUpdate(location.toLocationData()) } + override fun onProviderDisabled(provider: String) { + callback?.onLocationProviderIsNotAvailable() + } + private fun Location.toLocationData(): LocationData { return LocationData(latitude, longitude, accuracy.toDouble()) } From e3242f0debf8556229ac2c02ac9dfb6a9d000364 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Jan 2022 16:40:49 +0100 Subject: [PATCH 03/22] Prevent the dialog from being cancellable, since OK button finish the Activity. --- .../im/vector/app/features/location/LocationSharingFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index 900f465f04..b35a72029b 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -118,6 +118,7 @@ class LocationSharingFragment @Inject constructor( .setPositiveButton(R.string.ok) { _, _ -> activity?.finish() } + .setCancelable(false) .show() } From e9b9406bf11fd1069cd79a8a290400859e176861 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Jan 2022 21:11:20 +0100 Subject: [PATCH 04/22] Rework the location code - WIP --- .../timeline/helper/LocationPinProvider.kt | 35 +++++-- .../im/vector/app/features/location/Config.kt | 4 +- .../location/LocationPreviewFragment.kt | 37 ++++++- .../location/LocationSharingAction.kt | 2 - .../location/LocationSharingFragment.kt | 86 ++++++++-------- .../location/LocationSharingViewModel.kt | 36 +++++-- .../location/LocationSharingViewState.kt | 4 +- .../app/features/location/LocationTracker.kt | 72 ++++++++------ .../app/features/location/MapTilerMapView.kt | 98 ++++++++++++++----- .../app/features/location/VectorMapView.kt | 32 ------ 10 files changed, 247 insertions(+), 159 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/location/VectorMapView.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt index fe3a7d9007..e92376c44d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt @@ -28,6 +28,7 @@ import im.vector.app.core.glide.GlideApp import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.toMatrixItem +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -54,22 +55,36 @@ class LocationPinProvider @Inject constructor( val size = dimensionConverter.dpToPx(44) avatarRenderer.render(glideRequests, it, object : CustomTarget(size, size) { override fun onResourceReady(resource: Drawable, transition: Transition?) { - val bgUserPin = ContextCompat.getDrawable(context, R.drawable.bg_map_user_pin)!! - val layerDrawable = LayerDrawable(arrayOf(bgUserPin, resource)) - val horizontalInset = dimensionConverter.dpToPx(4) - val topInset = dimensionConverter.dpToPx(4) - val bottomInset = dimensionConverter.dpToPx(8) - layerDrawable.setLayerInset(1, horizontalInset, topInset, horizontalInset, bottomInset) - - cache[userId] = layerDrawable - - callback(layerDrawable) + Timber.d("## Location: onResourceReady") + val pinDrawable = createPinDrawable(resource) + cache[userId] = pinDrawable + callback(pinDrawable) } override fun onLoadCleared(placeholder: Drawable?) { // Is it possible? Put placeholder instead? + // FIXME The doc says it has to be implemented and should free resources + Timber.d("## Location: onLoadCleared") + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + Timber.w("## Location: onLoadFailed") + errorDrawable ?: return + val pinDrawable = createPinDrawable(errorDrawable) + cache[userId] = pinDrawable + callback(pinDrawable) } }) } } + + private fun createPinDrawable(drawable: Drawable): Drawable { + val bgUserPin = ContextCompat.getDrawable(context, R.drawable.bg_map_user_pin)!! + val layerDrawable = LayerDrawable(arrayOf(bgUserPin, drawable)) + val horizontalInset = dimensionConverter.dpToPx(4) + val topInset = dimensionConverter.dpToPx(4) + val bottomInset = dimensionConverter.dpToPx(8) + layerDrawable.setLayerInset(1, horizontalInset, topInset, horizontalInset, bottomInset) + return layerDrawable + } } diff --git a/vector/src/main/java/im/vector/app/features/location/Config.kt b/vector/src/main/java/im/vector/app/features/location/Config.kt index 630df16a37..0b3eb015b9 100644 --- a/vector/src/main/java/im/vector/app/features/location/Config.kt +++ b/vector/src/main/java/im/vector/app/features/location/Config.kt @@ -17,5 +17,5 @@ package im.vector.app.features.location const val INITIAL_MAP_ZOOM = 15.0 -const val MIN_TIME_MILLIS_TO_UPDATE_LOCATION = 1 * 60 * 1000L // every 1 minute -const val MIN_DISTANCE_METERS_TO_UPDATE_LOCATION = 10f +const val MIN_TIME_TO_UPDATE_LOCATION_MILLIS = 5 * 1_000L // every 5 seconds +const val MIN_DISTANCE_TO_UPDATE_LOCATION_METERS = 10f diff --git a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt index 6209bf5a4f..641b5910a6 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt @@ -22,19 +22,27 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import com.airbnb.mvrx.args +import com.mapbox.mapboxsdk.maps.MapView import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.openLocation import im.vector.app.databinding.FragmentLocationPreviewBinding import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import java.lang.ref.WeakReference import javax.inject.Inject +/** + * TODO Move locationPinProvider to a ViewModel + */ class LocationPreviewFragment @Inject constructor( private val locationPinProvider: LocationPinProvider ) : VectorBaseFragment() { private val args: LocationSharingArgs by args() + // Keep a ref to handle properly the onDestroy callback + private var mapView: WeakReference? = null + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationPreviewBinding { return FragmentLocationPreviewBinding.inflate(layoutInflater, container, false) } @@ -42,6 +50,8 @@ class LocationPreviewFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + mapView = WeakReference(views.mapView) + views.mapView.onCreate(savedInstanceState) views.mapView.initialize { if (isAdded) { onMapReady() @@ -49,16 +59,42 @@ class LocationPreviewFragment @Inject constructor( } } + override fun onResume() { + super.onResume() + views.mapView.onResume() + } + override fun onPause() { views.mapView.onPause() super.onPause() } + override fun onLowMemory() { + views.mapView.onLowMemory() + super.onLowMemory() + } + + override fun onStart() { + super.onStart() + views.mapView.onStart() + } + override fun onStop() { views.mapView.onStop() super.onStop() } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + views.mapView.onSaveInstanceState(outState) + } + + override fun onDestroy() { + mapView?.get()?.onDestroy() + mapView?.clear() + super.onDestroy() + } + override fun getMenuRes() = R.menu.menu_location_preview override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -85,7 +121,6 @@ class LocationPreviewFragment @Inject constructor( locationPinProvider.create(userId) { pinDrawable -> views.mapView.apply { zoomToLocation(location.latitude, location.longitude, INITIAL_MAP_ZOOM) - deleteAllPins() addPinToMap(userId, pinDrawable) updatePinLocation(userId, location.latitude, location.longitude) } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt index 71101d0612..01319ef6c7 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt @@ -19,7 +19,5 @@ package im.vector.app.features.location import im.vector.app.core.platform.VectorViewModelAction sealed class LocationSharingAction : VectorViewModelAction { - data class OnLocationUpdate(val locationData: LocationData) : LocationSharingAction() object OnShareLocation : LocationSharingAction() - object OnLocationProviderIsNotAvailable : LocationSharingAction() } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index b35a72029b..2ac8200f60 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -21,29 +21,29 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.mapbox.mapboxsdk.maps.MapView import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentLocationSharingBinding -import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider -import org.matrix.android.sdk.api.session.Session +import java.lang.ref.WeakReference import javax.inject.Inject +/** + * We should consider using SupportMapFragment for a out of the box lifecycle handling + */ class LocationSharingFragment @Inject constructor( - private val locationTracker: LocationTracker, - private val session: Session, - private val locationPinProvider: LocationPinProvider -) : VectorBaseFragment(), LocationTracker.Callback { - - init { - locationTracker.callback = this - } +) : VectorBaseFragment() { private val viewModel: LocationSharingViewModel by fragmentViewModel() private var lastZoomValue: Double = -1.0 + // Keep a ref to handle properly the onDestroy callback + private var mapView: WeakReference? = null + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationSharingBinding { return FragmentLocationSharingBinding.inflate(inflater, container, false) } @@ -51,11 +51,9 @@ class LocationSharingFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - views.mapView.initialize { - if (isAdded) { - onMapReady() - } - } + mapView = WeakReference(views.mapView) + views.mapView.onCreate(savedInstanceState) + views.mapView.initialize() views.shareLocationContainer.debouncedClicks { viewModel.handle(LocationSharingAction.OnShareLocation) @@ -63,54 +61,48 @@ class LocationSharingFragment @Inject constructor( viewModel.observeViewEvents { when (it) { - LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError() - LocationSharingViewEvents.Close -> activity?.finish() + LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError() + LocationSharingViewEvents.Close -> activity?.finish() }.exhaustive } } + override fun onResume() { + super.onResume() + views.mapView.onResume() + } + override fun onPause() { views.mapView.onPause() super.onPause() } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + views.mapView.onSaveInstanceState(outState) + } + + override fun onStart() { + super.onStart() + views.mapView.onStart() + } + override fun onStop() { views.mapView.onStop() super.onStop() } + override fun onLowMemory() { + super.onLowMemory() + views.mapView.onLowMemory() + } + override fun onDestroy() { - locationTracker.stop() + mapView?.get()?.onDestroy() + mapView?.clear() super.onDestroy() } - private fun onMapReady() { - if (!isAdded) return - - locationPinProvider.create(session.myUserId) { - views.mapView.addPinToMap( - pinId = USER_PIN_NAME, - image = it, - ) - // All set, start location tracker - locationTracker.start() - } - } - - override fun onLocationUpdate(locationData: LocationData) { - lastZoomValue = if (lastZoomValue == -1.0) INITIAL_MAP_ZOOM else views.mapView.getCurrentZoom() ?: INITIAL_MAP_ZOOM - - views.mapView.zoomToLocation(locationData.latitude, locationData.longitude, lastZoomValue) - views.mapView.deleteAllPins() - views.mapView.updatePinLocation(USER_PIN_NAME, locationData.latitude, locationData.longitude) - - viewModel.handle(LocationSharingAction.OnLocationUpdate(locationData)) - } - - override fun onLocationProviderIsNotAvailable() { - viewModel.handle(LocationSharingAction.OnLocationProviderIsNotAvailable) - } - private fun handleLocationNotAvailableError() { MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.location_not_available_dialog_title) @@ -122,6 +114,10 @@ class LocationSharingFragment @Inject constructor( .show() } + override fun invalidate() = withState(viewModel) { state -> + views.mapView.render(state) + } + companion object { const val USER_PIN_NAME = "USER_PIN_NAME" } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt index b3c97310e1..f4e1fd0281 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt @@ -24,12 +24,15 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import org.matrix.android.sdk.api.session.Session class LocationSharingViewModel @AssistedInject constructor( @Assisted private val initialState: LocationSharingViewState, - session: Session -) : VectorViewModel(initialState) { + private val locationTracker: LocationTracker, + private val locationPinProvider: LocationPinProvider, + private val session: Session +) : VectorViewModel(initialState), LocationTracker.Callback { private val room = session.getRoom(initialState.roomId)!! @@ -38,14 +41,31 @@ class LocationSharingViewModel @AssistedInject constructor( override fun create(initialState: LocationSharingViewState): LocationSharingViewModel } - companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + init { + locationTracker.start(this) + createPin() + } + + private fun createPin() { + locationPinProvider.create(session.myUserId) { + setState { + copy( + pinDrawable = it + ) + } + } + } + + override fun onCleared() { + super.onCleared() + locationTracker.stop() } override fun handle(action: LocationSharingAction) { when (action) { - is LocationSharingAction.OnLocationUpdate -> handleLocationUpdate(action.locationData) - LocationSharingAction.OnShareLocation -> handleShareLocation() - LocationSharingAction.OnLocationProviderIsNotAvailable -> handleLocationProviderIsNotAvailable() + LocationSharingAction.OnShareLocation -> handleShareLocation() }.exhaustive } @@ -62,13 +82,13 @@ class LocationSharingViewModel @AssistedInject constructor( } } - private fun handleLocationUpdate(locationData: LocationData) { + override fun onLocationUpdate(locationData: LocationData) { setState { copy(lastKnownLocation = locationData) } } - private fun handleLocationProviderIsNotAvailable() { + override fun onLocationProviderIsNotAvailable() { _viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError) } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt index 2869929b12..7ee5ba00fa 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt @@ -16,6 +16,7 @@ package im.vector.app.features.location +import android.graphics.drawable.Drawable import androidx.annotation.StringRes import com.airbnb.mvrx.MavericksState import im.vector.app.R @@ -28,7 +29,8 @@ enum class LocationSharingMode(@StringRes val titleRes: Int) { data class LocationSharingViewState( val roomId: String, val mode: LocationSharingMode, - val lastKnownLocation: LocationData? = null + val lastKnownLocation: LocationData? = null, + val pinDrawable: Drawable? = null ) : MavericksState { constructor(locationSharingArgs: LocationSharingArgs) : this( diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 9ccf79de33..9d3d506a73 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -27,63 +27,73 @@ import timber.log.Timber import javax.inject.Inject class LocationTracker @Inject constructor( - private val context: Context + context: Context ) : LocationListenerCompat { + private val locationManager = context.getSystemService() + interface Callback { fun onLocationUpdate(locationData: LocationData) fun onLocationProviderIsNotAvailable() } - private var locationManager: LocationManager? = null - var callback: Callback? = null + private var callback: Callback? = null @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) - fun start() { - val locationManager = context.getSystemService() + fun start(callback: Callback?) { + Timber.d("## LocationTracker. start()") + this.callback = callback - locationManager?.let { - val isGpsEnabled = it.isProviderEnabled(LocationManager.GPS_PROVIDER) - val isNetworkEnabled = it.isProviderEnabled(LocationManager.NETWORK_PROVIDER) - - val provider = when { - isGpsEnabled -> LocationManager.GPS_PROVIDER - isNetworkEnabled -> LocationManager.NETWORK_PROVIDER - else -> { - callback?.onLocationProviderIsNotAvailable() - Timber.v("## LocationTracker. There is no location provider available") - return - } - } - - // Send last known location without waiting location updates - it.getLastKnownLocation(provider)?.let { lastKnownLocation -> - callback?.onLocationUpdate(lastKnownLocation.toLocationData()) - } - - it.requestLocationUpdates( - provider, - MIN_TIME_MILLIS_TO_UPDATE_LOCATION, - MIN_DISTANCE_METERS_TO_UPDATE_LOCATION, - this - ) - } ?: run { + if (locationManager == null) { callback?.onLocationProviderIsNotAvailable() Timber.v("## LocationTracker. LocationManager is not available") + return } + + val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) + val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + + Timber.d("## LocationTracker. isGpsEnabled: $isGpsEnabled - isNetworkEnabled: $isNetworkEnabled") + + val provider = when { + isGpsEnabled -> LocationManager.GPS_PROVIDER + isNetworkEnabled -> LocationManager.NETWORK_PROVIDER + else -> { + callback?.onLocationProviderIsNotAvailable() + Timber.v("## LocationTracker. There is no location provider available") + return + } + } + + // Send last known location without waiting location updates + locationManager.getLastKnownLocation(provider)?.let { lastKnownLocation -> + Timber.d("## LocationTracker. lastKnownLocation") + callback?.onLocationUpdate(lastKnownLocation.toLocationData()) + } + + Timber.d("## LocationTracker. track location using $provider") + locationManager.requestLocationUpdates( + provider, + MIN_TIME_TO_UPDATE_LOCATION_MILLIS, + MIN_DISTANCE_TO_UPDATE_LOCATION_METERS, + this + ) } @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) fun stop() { + Timber.d("## LocationTracker. stop()") locationManager?.removeUpdates(this) callback = null } override fun onLocationChanged(location: Location) { + Timber.d("## LocationTracker. onLocationChanged") callback?.onLocationUpdate(location.toLocationData()) } override fun onProviderDisabled(provider: String) { + Timber.d("## LocationTracker. onProviderDisabled: $provider") callback?.onLocationProviderIsNotAvailable() } diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt index c64af1ebaa..e22525485e 100644 --- a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt +++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt @@ -28,34 +28,47 @@ import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions import com.mapbox.mapboxsdk.style.layers.Property import im.vector.app.BuildConfig +import timber.log.Timber class MapTilerMapView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : MapView(context, attrs, defStyleAttr), VectorMapView { +) : MapView(context, attrs, defStyleAttr) { - private var map: MapboxMap? = null - private var symbolManager: SymbolManager? = null - private var style: Style? = null + private var pendingState: LocationSharingViewState? = null - override fun initialize(onMapReady: () -> Unit) { + data class MapRefs( + val map: MapboxMap, + val symbolManager: SymbolManager, + val style: Style + ) + + private var mapRefs: MapRefs? = null + private var initZoomDone = false + + // TODO Kept only for the bottom sheet usage + fun initialize(onMapReady: () -> Unit) { getMapAsync { map -> map.setStyle(styleUrl) { style -> - this.symbolManager = SymbolManager(this, map, style) - this.map = map - this.style = style + mapRefs = MapRefs( + map, + SymbolManager(this, map, style), + style + ) onMapReady() } } } - override fun addPinToMap(pinId: String, image: Drawable) { - style?.addImage(pinId, image) + // TODO Kept only for the bottom sheet usage + fun addPinToMap(pinId: String, image: Drawable) { + mapRefs?.style?.addImage(pinId, image) } - override fun updatePinLocation(pinId: String, latitude: Double, longitude: Double) { - symbolManager?.create( + // TODO Kept only for the bottom sheet usage + fun updatePinLocation(pinId: String, latitude: Double, longitude: Double) { + mapRefs?.symbolManager?.create( SymbolOptions() .withLatLng(LatLng(latitude, longitude)) .withIconImage(pinId) @@ -63,28 +76,59 @@ class MapTilerMapView @JvmOverloads constructor( ) } - override fun deleteAllPins() { - symbolManager?.deleteAll() + /** + * For location fragments + */ + fun initialize() { + Timber.d("## Location: initialize") + + getMapAsync { map -> + map.setStyle(styleUrl) { style -> + mapRefs = MapRefs( + map, + SymbolManager(this, map, style), + style + ) + pendingState?.let { render(it) } + pendingState = null + } + } } - override fun zoomToLocation(latitude: Double, longitude: Double, zoom: Double) { - map?.cameraPosition = CameraPosition.Builder() + fun render(data: LocationSharingViewState) { + val safeMapRefs = mapRefs ?: return Unit.also { + pendingState = data + } + + data.pinDrawable?.let { pinDrawable -> + if (safeMapRefs.style.getImage(LocationSharingFragment.USER_PIN_NAME) == null) { + safeMapRefs.style.addImage(LocationSharingFragment.USER_PIN_NAME, pinDrawable) + } + } + + data.lastKnownLocation?.let { locationData -> + if (!initZoomDone) { + zoomToLocation(locationData.latitude, locationData.longitude, INITIAL_MAP_ZOOM) + initZoomDone = true + } + + safeMapRefs.symbolManager.create( + SymbolOptions() + .withLatLng(LatLng(locationData.latitude, locationData.longitude)) + .withIconImage(LocationSharingFragment.USER_PIN_NAME) + .withIconAnchor(Property.ICON_ANCHOR_BOTTOM) + ) + } + } + + fun zoomToLocation(latitude: Double, longitude: Double, zoom: Double) { + Timber.d("## Location: zoomToLocation") + mapRefs?.map?.cameraPosition = CameraPosition.Builder() .target(LatLng(latitude, longitude)) .zoom(zoom) .build() } - override fun getCurrentZoom(): Double? { - return map?.cameraPosition?.zoom - } - - override fun onClick(callback: () -> Unit) { - map?.addOnMapClickListener { - callback() - true - } - } - companion object { private const val styleUrl = "https://api.maptiler.com/maps/streets/style.json?key=${BuildConfig.mapTilerKey}" } diff --git a/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt b/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt deleted file mode 100644 index 23b59bf99a..0000000000 --- a/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 - -import android.graphics.drawable.Drawable - -interface VectorMapView { - fun initialize(onMapReady: () -> Unit) - - fun addPinToMap(pinId: String, image: Drawable) - fun updatePinLocation(pinId: String, latitude: Double, longitude: Double) - fun deleteAllPins() - - fun zoomToLocation(latitude: Double, longitude: Double, zoom: Double) - fun getCurrentZoom(): Double? - - fun onClick(callback: () -> Unit) -} From 1f53945031edf96f6a9055c238d5a3a464c05865 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Jan 2022 22:10:19 +0100 Subject: [PATCH 05/22] Rework the location code - WIP --- .../BottomSheetMessagePreviewItem.kt | 29 +++++++--- .../timeline/item/MessageLocationItem.kt | 29 +++++++--- .../location/LocationPreviewFragment.kt | 25 ++++---- .../location/LocationSharingFragment.kt | 4 +- .../location/LocationSharingViewState.kt | 7 +++ .../vector/app/features/location/MapState.kt | 26 +++++++++ .../app/features/location/MapTilerMapView.kt | 58 ++++++------------- 7 files changed, 104 insertions(+), 74 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/location/MapState.kt diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index cdecd2d6c6..37e07a3448 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt @@ -34,6 +34,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvid import im.vector.app.features.home.room.detail.timeline.item.BindingOptions import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess import im.vector.app.features.location.LocationData +import im.vector.app.features.location.MapState import im.vector.app.features.location.MapTilerMapView import im.vector.app.features.media.ImageContentRenderer import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence @@ -99,15 +100,25 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel - holder.mapView.initialize { - if (holder.view.isAttachedToWindow) { - holder.mapView.zoomToLocation(location.latitude, location.longitude, 15.0) - locationPinProvider?.create(matrixItem.id) { pinDrawable -> - holder.mapView.addPinToMap(matrixItem.id, pinDrawable) - holder.mapView.updatePinLocation(matrixItem.id, location.latitude, location.longitude) - } - } + holder.mapView.initialize() + holder.mapView.render( + MapState( + zoomOnlyOnce = false, + pinLocationData = locationData, + pinId = matrixItem.id, + pinDrawable = null + ) + ) + locationPinProvider?.create(matrixItem.id) { pinDrawable -> + if (holder.view.isAttachedToWindow) { + holder.mapView.render( + MapState( + zoomOnlyOnce = false, + pinLocationData = locationData, + pinId = matrixItem.id, + pinDrawable = pinDrawable + ) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt index 3f030866a5..e5d86d09c7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt @@ -24,6 +24,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.location.LocationData +import im.vector.app.features.location.MapState import im.vector.app.features.location.MapTilerMapView @EpoxyModelClass(layout = R.layout.item_timeline_event_base) @@ -56,14 +57,25 @@ abstract class MessageLocationItem : AbsMessageItem( callback?.onMapClicked() } - holder.mapView.apply { - initialize { - zoomToLocation(location.latitude, location.longitude, INITIAL_ZOOM) - - locationPinProvider?.create(locationOwnerId) { pinDrawable -> - addPinToMap(locationOwnerId, pinDrawable) - updatePinLocation(locationOwnerId, location.latitude, location.longitude) - } + holder.mapView.initialize() + holder.mapView.render( + MapState( + zoomOnlyOnce = false, + pinLocationData = location, + pinId = locationOwnerId, + pinDrawable = null + ) + ) + locationPinProvider?.create(locationOwnerId) { pinDrawable -> + if (holder.view.isAttachedToWindow) { + holder.mapView.render( + MapState( + zoomOnlyOnce = false, + pinLocationData = location, + pinId = locationOwnerId, + pinDrawable = pinDrawable + ) + ) } } } @@ -78,6 +90,5 @@ abstract class MessageLocationItem : AbsMessageItem( companion object { private const val STUB_ID = R.id.messageContentLocationStub - private const val INITIAL_ZOOM = 15.0 } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt index 641b5910a6..01417fdca0 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt @@ -21,6 +21,7 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args import com.mapbox.mapboxsdk.maps.MapView import im.vector.app.R @@ -52,11 +53,8 @@ class LocationPreviewFragment @Inject constructor( mapView = WeakReference(views.mapView) views.mapView.onCreate(savedInstanceState) - views.mapView.initialize { - if (isAdded) { - onMapReady() - } - } + views.mapView.initialize() + loadPinDrawable() } override fun onResume() { @@ -112,17 +110,20 @@ class LocationPreviewFragment @Inject constructor( openLocation(requireActivity(), location.latitude, location.longitude) } - private fun onMapReady() { - if (!isAdded) return - + private fun loadPinDrawable() { val location = args.initialLocationData ?: return val userId = args.locationOwnerId locationPinProvider.create(userId) { pinDrawable -> - views.mapView.apply { - zoomToLocation(location.latitude, location.longitude, INITIAL_MAP_ZOOM) - addPinToMap(userId, pinDrawable) - updatePinLocation(userId, location.latitude, location.longitude) + lifecycleScope.launchWhenResumed { + views.mapView.render( + MapState( + zoomOnlyOnce = true, + pinLocationData = location, + pinId = args.locationOwnerId, + pinDrawable = pinDrawable + ) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index 2ac8200f60..4dff78b5ef 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -39,8 +39,6 @@ class LocationSharingFragment @Inject constructor( private val viewModel: LocationSharingViewModel by fragmentViewModel() - private var lastZoomValue: Double = -1.0 - // Keep a ref to handle properly the onDestroy callback private var mapView: WeakReference? = null @@ -115,7 +113,7 @@ class LocationSharingFragment @Inject constructor( } override fun invalidate() = withState(viewModel) { state -> - views.mapView.render(state) + views.mapView.render(state.toMapState()) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt index 7ee5ba00fa..f3b937855a 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt @@ -38,3 +38,10 @@ data class LocationSharingViewState( mode = locationSharingArgs.mode ) } + +fun LocationSharingViewState.toMapState() = MapState( + zoomOnlyOnce = true, + pinLocationData = lastKnownLocation, + pinId = LocationSharingFragment.USER_PIN_NAME, + pinDrawable = pinDrawable +) diff --git a/vector/src/main/java/im/vector/app/features/location/MapState.kt b/vector/src/main/java/im/vector/app/features/location/MapState.kt new file mode 100644 index 0000000000..d001457e4f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/MapState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import android.graphics.drawable.Drawable + +data class MapState( + val zoomOnlyOnce: Boolean, + val pinLocationData: LocationData? = null, + val pinId: String, + val pinDrawable: Drawable? = null +) diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt index e22525485e..be02b84516 100644 --- a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt +++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt @@ -17,7 +17,6 @@ package im.vector.app.features.location import android.content.Context -import android.graphics.drawable.Drawable import android.util.AttributeSet import com.mapbox.mapboxsdk.camera.CameraPosition import com.mapbox.mapboxsdk.geometry.LatLng @@ -29,6 +28,7 @@ import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions import com.mapbox.mapboxsdk.style.layers.Property import im.vector.app.BuildConfig import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean class MapTilerMapView @JvmOverloads constructor( context: Context, @@ -36,7 +36,7 @@ class MapTilerMapView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : MapView(context, attrs, defStyleAttr) { - private var pendingState: LocationSharingViewState? = null + private var pendingState: MapState? = null data class MapRefs( val map: MapboxMap, @@ -44,43 +44,18 @@ class MapTilerMapView @JvmOverloads constructor( val style: Style ) + private var isInitializing = AtomicBoolean(false) private var mapRefs: MapRefs? = null private var initZoomDone = false - // TODO Kept only for the bottom sheet usage - fun initialize(onMapReady: () -> Unit) { - getMapAsync { map -> - map.setStyle(styleUrl) { style -> - mapRefs = MapRefs( - map, - SymbolManager(this, map, style), - style - ) - onMapReady() - } - } - } - - // TODO Kept only for the bottom sheet usage - fun addPinToMap(pinId: String, image: Drawable) { - mapRefs?.style?.addImage(pinId, image) - } - - // TODO Kept only for the bottom sheet usage - fun updatePinLocation(pinId: String, latitude: Double, longitude: Double) { - mapRefs?.symbolManager?.create( - SymbolOptions() - .withLatLng(LatLng(latitude, longitude)) - .withIconImage(pinId) - .withIconAnchor(Property.ICON_ANCHOR_BOTTOM) - ) - } - /** * For location fragments */ fun initialize() { - Timber.d("## Location: initialize") + Timber.d("## Location: initialize $isInitializing") + if (isInitializing.getAndSet(true)) { + return + } getMapAsync { map -> map.setStyle(styleUrl) { style -> @@ -95,20 +70,21 @@ class MapTilerMapView @JvmOverloads constructor( } } - fun render(data: LocationSharingViewState) { + fun render(state: MapState) { val safeMapRefs = mapRefs ?: return Unit.also { - pendingState = data + pendingState = state } - data.pinDrawable?.let { pinDrawable -> - if (safeMapRefs.style.getImage(LocationSharingFragment.USER_PIN_NAME) == null) { + state.pinDrawable?.let { pinDrawable -> + if (safeMapRefs.style.isFullyLoaded && + safeMapRefs.style.getImage(LocationSharingFragment.USER_PIN_NAME) == null) { safeMapRefs.style.addImage(LocationSharingFragment.USER_PIN_NAME, pinDrawable) } } - data.lastKnownLocation?.let { locationData -> - if (!initZoomDone) { - zoomToLocation(locationData.latitude, locationData.longitude, INITIAL_MAP_ZOOM) + state.pinLocationData?.let { locationData -> + if (!initZoomDone || !state.zoomOnlyOnce) { + zoomToLocation(locationData.latitude, locationData.longitude) initZoomDone = true } @@ -121,11 +97,11 @@ class MapTilerMapView @JvmOverloads constructor( } } - fun zoomToLocation(latitude: Double, longitude: Double, zoom: Double) { + private fun zoomToLocation(latitude: Double, longitude: Double) { Timber.d("## Location: zoomToLocation") mapRefs?.map?.cameraPosition = CameraPosition.Builder() .target(LatLng(latitude, longitude)) - .zoom(zoom) + .zoom(INITIAL_MAP_ZOOM) .build() } From 55ed7373f513ef8023dc618c217eb8415d27e004 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Jan 2022 22:20:53 +0100 Subject: [PATCH 06/22] Rework the location code - WIP --- .../im/vector/app/features/location/MapTilerMapView.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt index be02b84516..8b64cf79d3 100644 --- a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt +++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt @@ -76,9 +76,10 @@ class MapTilerMapView @JvmOverloads constructor( } state.pinDrawable?.let { pinDrawable -> - if (safeMapRefs.style.isFullyLoaded && - safeMapRefs.style.getImage(LocationSharingFragment.USER_PIN_NAME) == null) { - safeMapRefs.style.addImage(LocationSharingFragment.USER_PIN_NAME, pinDrawable) + if (!safeMapRefs.style.isFullyLoaded || + safeMapRefs.style.getImage(state.pinId) == null) { + safeMapRefs.symbolManager.deleteAll() + safeMapRefs.style.addImage(state.pinId, pinDrawable) } } @@ -91,7 +92,7 @@ class MapTilerMapView @JvmOverloads constructor( safeMapRefs.symbolManager.create( SymbolOptions() .withLatLng(LatLng(locationData.latitude, locationData.longitude)) - .withIconImage(LocationSharingFragment.USER_PIN_NAME) + .withIconImage(state.pinId) .withIconAnchor(Property.ICON_ANCHOR_BOTTOM) ) } From 26c0fee56ddcf4376643b15940200b394eccd4c4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Jan 2022 22:27:43 +0100 Subject: [PATCH 07/22] Add a loader waiting for the user location to be known --- .../app/features/location/LocationSharingFragment.kt | 2 ++ vector/src/main/res/layout/fragment_location_sharing.xml | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index 4dff78b5ef..c18a3540d3 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isGone import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -114,6 +115,7 @@ class LocationSharingFragment @Inject constructor( override fun invalidate() = withState(viewModel) { state -> views.mapView.render(state.toMapState()) + views.shareLocationGpsLoading.isGone = state.lastKnownLocation != null } companion object { diff --git a/vector/src/main/res/layout/fragment_location_sharing.xml b/vector/src/main/res/layout/fragment_location_sharing.xml index f9a37a6241..ad418f3e1c 100644 --- a/vector/src/main/res/layout/fragment_location_sharing.xml +++ b/vector/src/main/res/layout/fragment_location_sharing.xml @@ -48,4 +48,13 @@ android:textColor="?colorPrimary" android:textStyle="bold" /> + + \ No newline at end of file From 0f8c3bcc17e39828459ae8c790886cd90dcd5ab0 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 28 Jan 2022 14:17:07 +0300 Subject: [PATCH 08/22] Try to get location by using all available providers. --- .../app/features/location/LocationTracker.kt | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 9d3d506a73..133819ba7a 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -55,29 +55,28 @@ class LocationTracker @Inject constructor( Timber.d("## LocationTracker. isGpsEnabled: $isGpsEnabled - isNetworkEnabled: $isNetworkEnabled") - val provider = when { - isGpsEnabled -> LocationManager.GPS_PROVIDER - isNetworkEnabled -> LocationManager.NETWORK_PROVIDER - else -> { - callback?.onLocationProviderIsNotAvailable() - Timber.v("## LocationTracker. There is no location provider available") - return - } - } + locationManager.allProviders + .takeIf { it.isNotEmpty() } + ?.forEach { provider -> + Timber.d("## LocationTracker. track location using $provider") - // Send last known location without waiting location updates - locationManager.getLastKnownLocation(provider)?.let { lastKnownLocation -> - Timber.d("## LocationTracker. lastKnownLocation") - callback?.onLocationUpdate(lastKnownLocation.toLocationData()) - } + // Send last known location without waiting location updates + locationManager.getLastKnownLocation(provider)?.let { lastKnownLocation -> + Timber.d("## LocationTracker. lastKnownLocation") + callback?.onLocationUpdate(lastKnownLocation.toLocationData()) + } - Timber.d("## LocationTracker. track location using $provider") - locationManager.requestLocationUpdates( - provider, - MIN_TIME_TO_UPDATE_LOCATION_MILLIS, - MIN_DISTANCE_TO_UPDATE_LOCATION_METERS, - this - ) + locationManager.requestLocationUpdates( + provider, + MIN_TIME_TO_UPDATE_LOCATION_MILLIS, + MIN_DISTANCE_TO_UPDATE_LOCATION_METERS, + this + ) + } + ?: run { + callback?.onLocationProviderIsNotAvailable() + Timber.v("## LocationTracker. There is no location provider available") + } } @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) From 4026ddb34fb7fc84b611c92182c12585e23273e8 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 28 Jan 2022 15:07:51 +0300 Subject: [PATCH 09/22] Fix multiple pin rendering. --- .../java/im/vector/app/features/location/LocationTracker.kt | 5 ----- .../java/im/vector/app/features/location/MapTilerMapView.kt | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 133819ba7a..ff66baf5ca 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -50,11 +50,6 @@ class LocationTracker @Inject constructor( return } - val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) - val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) - - Timber.d("## LocationTracker. isGpsEnabled: $isGpsEnabled - isNetworkEnabled: $isNetworkEnabled") - locationManager.allProviders .takeIf { it.isNotEmpty() } ?.forEach { provider -> diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt index 8b64cf79d3..8ef72dfdff 100644 --- a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt +++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt @@ -78,7 +78,6 @@ class MapTilerMapView @JvmOverloads constructor( state.pinDrawable?.let { pinDrawable -> if (!safeMapRefs.style.isFullyLoaded || safeMapRefs.style.getImage(state.pinId) == null) { - safeMapRefs.symbolManager.deleteAll() safeMapRefs.style.addImage(state.pinId, pinDrawable) } } @@ -89,6 +88,7 @@ class MapTilerMapView @JvmOverloads constructor( initZoomDone = true } + safeMapRefs.symbolManager.deleteAll() safeMapRefs.symbolManager.create( SymbolOptions() .withLatLng(LatLng(locationData.latitude, locationData.longitude)) From 50279e31832216aa39b787f7fd370702623638fc Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 28 Jan 2022 18:51:28 +0300 Subject: [PATCH 10/22] Use static map image in timeline. --- .../timeline/factory/MessageItemFactory.kt | 9 +++- .../timeline/item/MessageLocationItem.kt | 49 +++++++++---------- .../im/vector/app/features/location/Config.kt | 29 ++++++++++- .../location/LocationSharingFragment.kt | 3 +- .../app/features/location/MapTilerMapView.kt | 9 +--- .../item_timeline_event_location_stub.xml | 33 ++++++------- vector/src/main/res/values/strings.xml | 1 + 7 files changed, 77 insertions(+), 56 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index eab7621d14..b0bdef5636 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -16,6 +16,7 @@ package im.vector.app.features.home.room.detail.timeline.factory +import android.content.res.Resources import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned @@ -127,7 +128,8 @@ class MessageItemFactory @Inject constructor( private val session: Session, private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker, private val locationPinProvider: LocationPinProvider, - private val vectorPreferences: VectorPreferences) { + private val vectorPreferences: VectorPreferences, + private val resources: Resources) { // TODO inject this properly? private var roomId: String = "" @@ -207,11 +209,16 @@ class MessageItemFactory @Inject constructor( } } + val width = resources.displayMetrics.widthPixels - dimensionConverter.dpToPx(60) + val height = dimensionConverter.dpToPx(200) + return MessageLocationItem_() .attributes(attributes) .locationData(locationData) .userId(informationData.senderId) .locationPinProvider(locationPinProvider) + .mapWidth(width) + .mapHeight(height) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) .callback(mapCallback) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt index e5d86d09c7..4c32d933ca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt @@ -17,15 +17,17 @@ package im.vector.app.features.home.room.detail.timeline.item import android.widget.FrameLayout -import androidx.constraintlayout.widget.ConstraintLayout +import android.widget.ImageView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import com.bumptech.glide.request.RequestOptions import im.vector.app.R import im.vector.app.core.epoxy.onClick +import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE import im.vector.app.features.location.LocationData -import im.vector.app.features.location.MapState -import im.vector.app.features.location.MapTilerMapView +import im.vector.app.features.location.getStaticMapUrl @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageLocationItem : AbsMessageItem() { @@ -46,6 +48,12 @@ abstract class MessageLocationItem : AbsMessageItem( @EpoxyAttribute var locationPinProvider: LocationPinProvider? = null + @EpoxyAttribute + var mapWidth: Int = 1200 + + @EpoxyAttribute + var mapHeight: Int = 800 + override fun bind(holder: Holder) { super.bind(holder) renderSendState(holder.mapViewContainer, null) @@ -53,39 +61,28 @@ abstract class MessageLocationItem : AbsMessageItem( val location = locationData ?: return val locationOwnerId = userId ?: return - holder.clickableMapArea.onClick { + holder.mapViewContainer.onClick { callback?.onMapClicked() } - holder.mapView.initialize() - holder.mapView.render( - MapState( - zoomOnlyOnce = false, - pinLocationData = location, - pinId = locationOwnerId, - pinDrawable = null - ) - ) + GlideApp.with(holder.staticMapImageView) + .load(getStaticMapUrl(location.latitude, location.longitude, INITIAL_MAP_ZOOM_IN_TIMELINE, mapWidth, mapHeight)) + .apply(RequestOptions.centerCropTransform()) + .into(holder.staticMapImageView) + locationPinProvider?.create(locationOwnerId) { pinDrawable -> - if (holder.view.isAttachedToWindow) { - holder.mapView.render( - MapState( - zoomOnlyOnce = false, - pinLocationData = location, - pinId = locationOwnerId, - pinDrawable = pinDrawable - ) - ) - } + GlideApp.with(holder.staticMapPinImageView) + .load(pinDrawable) + .into(holder.staticMapPinImageView) } } override fun getViewType() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { - val mapViewContainer by bind(R.id.mapViewContainer) - val mapView by bind(R.id.mapView) - val clickableMapArea by bind(R.id.clickableMapArea) + val mapViewContainer by bind(R.id.mapViewContainer) + val staticMapImageView by bind(R.id.staticMapImageView) + val staticMapPinImageView by bind(R.id.staticMapPinImageView) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/location/Config.kt b/vector/src/main/java/im/vector/app/features/location/Config.kt index 0b3eb015b9..9b42c6d9b3 100644 --- a/vector/src/main/java/im/vector/app/features/location/Config.kt +++ b/vector/src/main/java/im/vector/app/features/location/Config.kt @@ -16,6 +16,33 @@ package im.vector.app.features.location -const val INITIAL_MAP_ZOOM = 15.0 +import im.vector.app.BuildConfig + +const val MAP_STYLE_URL = "https://api.maptiler.com/maps/streets/style.json?key=${BuildConfig.mapTilerKey}" +private const val STATIC_MAP_IMAGE_URL = "https://api.maptiler.com/maps/basic/static/" + +const val INITIAL_MAP_ZOOM_IN_PREVIEW = 15.0 +const val INITIAL_MAP_ZOOM_IN_TIMELINE = 17.0 const val MIN_TIME_TO_UPDATE_LOCATION_MILLIS = 5 * 1_000L // every 5 seconds const val MIN_DISTANCE_TO_UPDATE_LOCATION_METERS = 10f + +fun getStaticMapUrl(latitude: Double, + longitude: Double, + zoom: Double, + width: Int, + height: Int): String { + return buildString { + append(STATIC_MAP_IMAGE_URL) + append(longitude) + append(",") + append(latitude) + append(",") + append(zoom) + append("/") + append(width) + append("x") + append(height) + append(".png?key=") + append(BuildConfig.mapTilerKey) + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index c18a3540d3..900d70e825 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -35,8 +35,7 @@ import javax.inject.Inject /** * We should consider using SupportMapFragment for a out of the box lifecycle handling */ -class LocationSharingFragment @Inject constructor( -) : VectorBaseFragment() { +class LocationSharingFragment @Inject constructor() : VectorBaseFragment() { private val viewModel: LocationSharingViewModel by fragmentViewModel() diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt index 8ef72dfdff..4ff8c2ad4d 100644 --- a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt +++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt @@ -26,7 +26,6 @@ import com.mapbox.mapboxsdk.maps.Style import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions import com.mapbox.mapboxsdk.style.layers.Property -import im.vector.app.BuildConfig import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean @@ -58,7 +57,7 @@ class MapTilerMapView @JvmOverloads constructor( } getMapAsync { map -> - map.setStyle(styleUrl) { style -> + map.setStyle(MAP_STYLE_URL) { style -> mapRefs = MapRefs( map, SymbolManager(this, map, style), @@ -102,11 +101,7 @@ class MapTilerMapView @JvmOverloads constructor( Timber.d("## Location: zoomToLocation") mapRefs?.map?.cameraPosition = CameraPosition.Builder() .target(LatLng(latitude, longitude)) - .zoom(INITIAL_MAP_ZOOM) + .zoom(INITIAL_MAP_ZOOM_IN_PREVIEW) .build() } - - companion object { - private const val styleUrl = "https://api.maptiler.com/maps/streets/style.json?key=${BuildConfig.mapTilerKey}" - } } diff --git a/vector/src/main/res/layout/item_timeline_event_location_stub.xml b/vector/src/main/res/layout/item_timeline_event_location_stub.xml index b2f68b2fc3..3d5f856a6f 100644 --- a/vector/src/main/res/layout/item_timeline_event_location_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_location_stub.xml @@ -6,30 +6,25 @@ android:layout_height="wrap_content" app:cardCornerRadius="8dp"> - - + android:contentDescription="@string/a11y_static_map_image" /> - - - + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 22c890eb01..378b8d7cbf 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3711,6 +3711,7 @@ Share location Location Share location + Map Share location ${app_name} could not access your location ${app_name} could not access your location. Please try again later. From 2dc52da30c82cce9855ed037548f6a10d32adad8 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 28 Jan 2022 20:48:53 +0300 Subject: [PATCH 11/22] Use static map image in bottom sheet. --- .../BottomSheetMessagePreviewItem.kt | 53 ++++++++++--------- .../im/vector/app/features/location/Config.kt | 1 + .../item_bottom_sheet_message_preview.xml | 28 +++++++--- 3 files changed, 51 insertions(+), 31 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index 37e07a3448..8805c07109 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt @@ -17,25 +17,28 @@ package im.vector.app.core.epoxy.bottomsheet import android.text.method.MovementMethod +import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import com.bumptech.glide.request.RequestOptions import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.glide.GlideApp import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.item.BindingOptions import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess +import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE import im.vector.app.features.location.LocationData -import im.vector.app.features.location.MapState -import im.vector.app.features.location.MapTilerMapView +import im.vector.app.features.location.getStaticMapUrl import im.vector.app.features.media.ImageContentRenderer import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.util.MatrixItem @@ -82,6 +85,12 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel - if (holder.view.isAttachedToWindow) { - holder.mapView.render( - MapState( - zoomOnlyOnce = false, - pinLocationData = locationData, - pinId = matrixItem.id, - pinDrawable = pinDrawable - ) - ) + if (locationData == null) { + holder.body.isVisible = true + holder.mapViewContainer.isVisible = false + } else { + holder.body.isVisible = false + holder.mapViewContainer.isVisible = true + GlideApp.with(holder.staticMapImageView) + .load(getStaticMapUrl(locationData!!.latitude, locationData!!.longitude, INITIAL_MAP_ZOOM_IN_TIMELINE, mapWidth, mapHeight)) + .apply(RequestOptions.centerCropTransform()) + .into(holder.staticMapImageView) + + locationPinProvider?.create(matrixItem.id) { pinDrawable -> + GlideApp.with(holder.staticMapPinImageView) + .load(pinDrawable) + .into(holder.staticMapPinImageView) } } } @@ -135,6 +138,8 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel(R.id.bottom_sheet_message_preview_body_details) val timestamp by bind(R.id.bottom_sheet_message_preview_timestamp) val imagePreview by bind(R.id.bottom_sheet_message_preview_image) - val mapView by bind(R.id.bottom_sheet_message_preview_location) + val mapViewContainer by bind(R.id.mapViewContainer) + val staticMapImageView by bind(R.id.staticMapImageView) + val staticMapPinImageView by bind(R.id.staticMapPinImageView) } } diff --git a/vector/src/main/java/im/vector/app/features/location/Config.kt b/vector/src/main/java/im/vector/app/features/location/Config.kt index 9b42c6d9b3..eb5f1ac3b7 100644 --- a/vector/src/main/java/im/vector/app/features/location/Config.kt +++ b/vector/src/main/java/im/vector/app/features/location/Config.kt @@ -44,5 +44,6 @@ fun getStaticMapUrl(latitude: Double, append(height) append(".png?key=") append(BuildConfig.mapTilerKey) + append("&attribution=bottomleft") } } diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml index 95e6975803..5dddfaa2c8 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml @@ -103,18 +103,32 @@ tools:text="1080 x 1024 - 43s - 12kB" tools:visibility="visible" /> - + tools:visibility="visible"> + + + + + From e0ac8ee8b8dc01e1612144856dfabefad062dcd8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 28 Jan 2022 18:23:31 +0100 Subject: [PATCH 12/22] No need for an extra FrameLayout --- .../timeline/item/MessageLocationItem.kt | 6 ++-- .../item_timeline_event_location_stub.xml | 30 ++++++++----------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt index 4c32d933ca..619d799ec5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt @@ -16,7 +16,6 @@ package im.vector.app.features.home.room.detail.timeline.item -import android.widget.FrameLayout import android.widget.ImageView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass @@ -56,12 +55,12 @@ abstract class MessageLocationItem : AbsMessageItem( override fun bind(holder: Holder) { super.bind(holder) - renderSendState(holder.mapViewContainer, null) + renderSendState(holder.view, null) val location = locationData ?: return val locationOwnerId = userId ?: return - holder.mapViewContainer.onClick { + holder.view.onClick { callback?.onMapClicked() } @@ -80,7 +79,6 @@ abstract class MessageLocationItem : AbsMessageItem( override fun getViewType() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { - val mapViewContainer by bind(R.id.mapViewContainer) val staticMapImageView by bind(R.id.staticMapImageView) val staticMapPinImageView by bind(R.id.staticMapPinImageView) } diff --git a/vector/src/main/res/layout/item_timeline_event_location_stub.xml b/vector/src/main/res/layout/item_timeline_event_location_stub.xml index 3d5f856a6f..316470b5f1 100644 --- a/vector/src/main/res/layout/item_timeline_event_location_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_location_stub.xml @@ -6,25 +6,19 @@ android:layout_height="wrap_content" app:cardCornerRadius="8dp"> - + android:layout_height="200dp" + android:contentDescription="@string/a11y_static_map_image" /> - - - - + From eff6942f82019953efd09f80215c2f688b7445a2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 28 Jan 2022 18:56:09 +0100 Subject: [PATCH 13/22] Use a MaterialCarView --- .../main/res/layout/item_bottom_sheet_message_preview.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml index 5dddfaa2c8..e40760e046 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml @@ -104,15 +104,17 @@ tools:visibility="visible" /> - - + From 2ce389456281b3c1c5890acc0482893792c21678 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 28 Jan 2022 22:46:58 +0100 Subject: [PATCH 14/22] Create a UrlMapProvider for a better handling of RTL languages, and build the URLs in the controllers --- .../src/main/res/values-ldrtl/bools.xml | 6 ++ .../ui-styles/src/main/res/values/bools.xml | 2 + .../BottomSheetMessagePreviewItem.kt | 15 +---- .../action/MessageActionsEpoxyController.kt | 13 +++-- .../timeline/factory/MessageItemFactory.kt | 14 +++-- .../timeline/item/MessageLocationItem.kt | 15 +---- .../im/vector/app/features/location/Config.kt | 28 +-------- .../location/LocationPreviewFragment.kt | 3 +- .../location/LocationSharingFragment.kt | 6 +- .../app/features/location/MapTilerMapView.kt | 12 +--- .../app/features/location/UrlMapProvider.kt | 58 +++++++++++++++++++ 11 files changed, 102 insertions(+), 70 deletions(-) create mode 100644 library/ui-styles/src/main/res/values-ldrtl/bools.xml create mode 100644 vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt diff --git a/library/ui-styles/src/main/res/values-ldrtl/bools.xml b/library/ui-styles/src/main/res/values-ldrtl/bools.xml new file mode 100644 index 0000000000..27b280985f --- /dev/null +++ b/library/ui-styles/src/main/res/values-ldrtl/bools.xml @@ -0,0 +1,6 @@ + + + + true + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/bools.xml b/library/ui-styles/src/main/res/values/bools.xml index 93d5f925af..9966999f28 100644 --- a/library/ui-styles/src/main/res/values/bools.xml +++ b/library/ui-styles/src/main/res/values/bools.xml @@ -4,4 +4,6 @@ false + false + \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index 8805c07109..5295cbaec3 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt @@ -36,9 +36,6 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.item.BindingOptions import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess -import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE -import im.vector.app.features.location.LocationData -import im.vector.app.features.location.getStaticMapUrl import im.vector.app.features.media.ImageContentRenderer import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.util.MatrixItem @@ -74,7 +71,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel() { @@ -74,9 +77,11 @@ class MessageActionsEpoxyController @Inject constructor( val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL) val body = state.messageBody.linkify(host.listener) val bindingOptions = spanUtils.getBindingOptions(body) - val locationData = state.timelineEvent()?.root?.getClearContent()?.toModel(catchError = true)?.let { - LocationData.create(it.getUri()) - } + val locationUrl = state.timelineEvent()?.root?.getClearContent() + ?.toModel(catchError = true) + ?.let { LocationData.create(it.getUri()) } + ?.let { urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, 1200, 800) } + bottomSheetMessagePreviewItem { id("preview") avatarRenderer(host.avatarRenderer) @@ -89,7 +94,7 @@ class MessageActionsEpoxyController @Inject constructor( body(body.toEpoxyCharSequence()) bodyDetails(host.eventDetailsFormatter.format(state.timelineEvent()?.root)?.toEpoxyCharSequence()) time(formattedDate) - locationData(locationData) + locationUrl(locationUrl) locationPinProvider(host.locationPinProvider) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index b0bdef5636..8355aeed90 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -72,7 +72,9 @@ import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.SpanUtils import im.vector.app.features.html.VectorHtmlCompressor +import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE import im.vector.app.features.location.LocationData +import im.vector.app.features.location.UrlMapProvider import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.settings.VectorPreferences @@ -129,7 +131,9 @@ class MessageItemFactory @Inject constructor( private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker, private val locationPinProvider: LocationPinProvider, private val vectorPreferences: VectorPreferences, - private val resources: Resources) { + private val urlMapProvider: UrlMapProvider, + private val resources: Resources +) { // TODO inject this properly? private var roomId: String = "" @@ -212,13 +216,15 @@ class MessageItemFactory @Inject constructor( val width = resources.displayMetrics.widthPixels - dimensionConverter.dpToPx(60) val height = dimensionConverter.dpToPx(200) + val locationUrl = locationData?.let { + urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height) + } + return MessageLocationItem_() .attributes(attributes) - .locationData(locationData) + .locationUrl(locationUrl) .userId(informationData.senderId) .locationPinProvider(locationPinProvider) - .mapWidth(width) - .mapHeight(height) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) .callback(mapCallback) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt index 619d799ec5..1e8ce4fb4d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt @@ -24,9 +24,6 @@ import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider -import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE -import im.vector.app.features.location.LocationData -import im.vector.app.features.location.getStaticMapUrl @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageLocationItem : AbsMessageItem() { @@ -39,7 +36,7 @@ abstract class MessageLocationItem : AbsMessageItem( var callback: Callback? = null @EpoxyAttribute - var locationData: LocationData? = null + var locationUrl: String? = null @EpoxyAttribute var userId: String? = null @@ -47,17 +44,11 @@ abstract class MessageLocationItem : AbsMessageItem( @EpoxyAttribute var locationPinProvider: LocationPinProvider? = null - @EpoxyAttribute - var mapWidth: Int = 1200 - - @EpoxyAttribute - var mapHeight: Int = 800 - override fun bind(holder: Holder) { super.bind(holder) renderSendState(holder.view, null) - val location = locationData ?: return + val location = locationUrl ?: return val locationOwnerId = userId ?: return holder.view.onClick { @@ -65,7 +56,7 @@ abstract class MessageLocationItem : AbsMessageItem( } GlideApp.with(holder.staticMapImageView) - .load(getStaticMapUrl(location.latitude, location.longitude, INITIAL_MAP_ZOOM_IN_TIMELINE, mapWidth, mapHeight)) + .load(location) .apply(RequestOptions.centerCropTransform()) .into(holder.staticMapImageView) diff --git a/vector/src/main/java/im/vector/app/features/location/Config.kt b/vector/src/main/java/im/vector/app/features/location/Config.kt index eb5f1ac3b7..29ca6b81a9 100644 --- a/vector/src/main/java/im/vector/app/features/location/Config.kt +++ b/vector/src/main/java/im/vector/app/features/location/Config.kt @@ -16,34 +16,10 @@ package im.vector.app.features.location -import im.vector.app.BuildConfig - -const val MAP_STYLE_URL = "https://api.maptiler.com/maps/streets/style.json?key=${BuildConfig.mapTilerKey}" -private const val STATIC_MAP_IMAGE_URL = "https://api.maptiler.com/maps/basic/static/" +const val MAP_BASE_URL = "https://api.maptiler.com/maps/streets/style.json" +const val STATIC_MAP_BASE_URL = "https://api.maptiler.com/maps/basic/static/" const val INITIAL_MAP_ZOOM_IN_PREVIEW = 15.0 const val INITIAL_MAP_ZOOM_IN_TIMELINE = 17.0 const val MIN_TIME_TO_UPDATE_LOCATION_MILLIS = 5 * 1_000L // every 5 seconds const val MIN_DISTANCE_TO_UPDATE_LOCATION_METERS = 10f - -fun getStaticMapUrl(latitude: Double, - longitude: Double, - zoom: Double, - width: Int, - height: Int): String { - return buildString { - append(STATIC_MAP_IMAGE_URL) - append(longitude) - append(",") - append(latitude) - append(",") - append(zoom) - append("/") - append(width) - append("x") - append(height) - append(".png?key=") - append(BuildConfig.mapTilerKey) - append("&attribution=bottomleft") - } -} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt index 01417fdca0..c4f2f148bf 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt @@ -36,6 +36,7 @@ import javax.inject.Inject * TODO Move locationPinProvider to a ViewModel */ class LocationPreviewFragment @Inject constructor( + private val urlMapProvider: UrlMapProvider, private val locationPinProvider: LocationPinProvider ) : VectorBaseFragment() { @@ -53,7 +54,7 @@ class LocationPreviewFragment @Inject constructor( mapView = WeakReference(views.mapView) views.mapView.onCreate(savedInstanceState) - views.mapView.initialize() + views.mapView.initialize(urlMapProvider.mapUrl) loadPinDrawable() } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index 900d70e825..f6bad2826b 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -35,7 +35,9 @@ import javax.inject.Inject /** * We should consider using SupportMapFragment for a out of the box lifecycle handling */ -class LocationSharingFragment @Inject constructor() : VectorBaseFragment() { +class LocationSharingFragment @Inject constructor( + private val urlMapProvider: UrlMapProvider +) : VectorBaseFragment() { private val viewModel: LocationSharingViewModel by fragmentViewModel() @@ -51,7 +53,7 @@ class LocationSharingFragment @Inject constructor() : VectorBaseFragment - map.setStyle(MAP_STYLE_URL) { style -> + map.setStyle(url) { style -> mapRefs = MapRefs( map, SymbolManager(this, map, style), diff --git a/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt b/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt new file mode 100644 index 0000000000..76d44f5ece --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import android.content.res.Resources +import im.vector.app.BuildConfig +import im.vector.app.R +import javax.inject.Inject + +class UrlMapProvider @Inject constructor( + private val resources: Resources +) { + private val keyParam = "?key=${BuildConfig.mapTilerKey}" + + // This is static so no need for a fun + val mapUrl = buildString { + append(MAP_BASE_URL) + append(keyParam) + } + + fun buildStaticMapUrl(locationData: LocationData, + zoom: Double, + width: Int, + height: Int): String { + return buildString { + append(STATIC_MAP_BASE_URL) + append(locationData.longitude) + append(",") + append(locationData.latitude) + append(",") + append(zoom) + append("/") + append(width) + append("x") + append(height) + append(".png") + append(keyParam) + if (!resources.getBoolean(R.bool.is_rtl)) { + // On LTR languages we want the legal mentions to be displayed on the bottom left of the image + append("&attribution=bottomleft") + } + } + } +} From b14e557c36deef390fa3973f34c8d3cf568ba75b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 29 Jan 2022 07:36:19 +0100 Subject: [PATCH 15/22] Use the existing item click mechanism --- .../home/room/detail/RoomDetailAction.kt | 3 --- .../home/room/detail/RoomDetailFragment.kt | 16 ++++++++++++---- .../home/room/detail/RoomDetailViewEvents.kt | 2 -- .../home/room/detail/RoomDetailViewModel.kt | 5 ----- .../timeline/factory/MessageItemFactory.kt | 13 +------------ .../detail/timeline/item/MessageLocationItem.kt | 12 ------------ 6 files changed, 13 insertions(+), 38 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 58e36d2303..18d9994cd5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -112,7 +112,4 @@ sealed class RoomDetailAction : VectorViewModelAction { // Poll data class EndPoll(val eventId: String) : RoomDetailAction() - - // Location - data class ShowLocation(val locationData: LocationData, val userId: String) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 9926ecad24..7e2bb835e0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -481,7 +481,6 @@ class RoomDetailFragment @Inject constructor( RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() - is RoomDetailViewEvents.ShowLocation -> handleShowLocationPreview(it) }.exhaustive } @@ -613,14 +612,17 @@ class RoomDetailFragment @Inject constructor( } } - private fun handleShowLocationPreview(viewEvent: RoomDetailViewEvents.ShowLocation) { + private fun handleShowLocationPreview(locationContent: MessageLocationContent, senderId: String) { + // TODO Create a helper + val geoUri = locationContent.getUri() + val locationData = LocationData.create(geoUri) navigator .openLocationSharing( context = requireContext(), roomId = roomDetailArgs.roomId, mode = LocationSharingMode.PREVIEW, - initialLocationData = viewEvent.locationData, - locationOwnerId = viewEvent.userId + initialLocationData = locationData, + locationOwnerId = senderId ) } @@ -1828,6 +1830,12 @@ class RoomDetailFragment @Inject constructor( is EncryptedEventContent -> { roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId)) } + is MessageLocationContent -> { + handleShowLocationPreview(messageContent, informationData.senderId) + } + else -> { + Timber.d("No click action defined for this message content") + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index b0921e01f9..ee823adc17 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -83,6 +83,4 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() object StopChatEffects : RoomDetailViewEvents() object RoomReplacementStarted : RoomDetailViewEvents() - - data class ShowLocation(val locationData: LocationData, val userId: String) : RoomDetailViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 6e14b0fc76..1bb70e29d7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -385,14 +385,9 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true)) } is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId) - is RoomDetailAction.ShowLocation -> handleShowLocation(action.locationData, action.userId) }.exhaustive } - private fun handleShowLocation(locationData: LocationData, userId: String) { - _viewEvents.post(RoomDetailViewEvents.ShowLocation(locationData, userId)) - } - private fun handleJitsiCallJoinStatus(action: RoomDetailAction.UpdateJoinJitsiCallStatus) = withState { state -> if (state.jitsiState.confId == null) { // If jitsi widget is removed while on the call diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 8355aeed90..0b0373a889 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -34,7 +34,6 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.containsOnlyEmojis -import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder @@ -188,7 +187,7 @@ class MessageItemFactory @Inject constructor( is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) is MessageLocationContent -> { if (vectorPreferences.labsRenderLocationsInTimeline()) { - buildLocationItem(messageContent, informationData, highlight, callback, attributes) + buildLocationItem(messageContent, informationData, highlight, attributes) } else { buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) } @@ -200,19 +199,10 @@ class MessageItemFactory @Inject constructor( private fun buildLocationItem(locationContent: MessageLocationContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageLocationItem? { val geoUri = locationContent.getUri() val locationData = LocationData.create(geoUri) - val mapCallback: MessageLocationItem.Callback = object : MessageLocationItem.Callback { - override fun onMapClicked() { - locationData?.let { - callback?.onTimelineItemAction(RoomDetailAction.ShowLocation(it, informationData.senderId)) - } - } - } - val width = resources.displayMetrics.widthPixels - dimensionConverter.dpToPx(60) val height = dimensionConverter.dpToPx(200) @@ -227,7 +217,6 @@ class MessageItemFactory @Inject constructor( .locationPinProvider(locationPinProvider) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) - .callback(mapCallback) } private fun buildPollItem(pollContent: MessagePollContent, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt index 1e8ce4fb4d..6f0b6abb72 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt @@ -21,20 +21,12 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import com.bumptech.glide.request.RequestOptions import im.vector.app.R -import im.vector.app.core.epoxy.onClick import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageLocationItem : AbsMessageItem() { - interface Callback { - fun onMapClicked() - } - - @EpoxyAttribute - var callback: Callback? = null - @EpoxyAttribute var locationUrl: String? = null @@ -51,10 +43,6 @@ abstract class MessageLocationItem : AbsMessageItem( val location = locationUrl ?: return val locationOwnerId = userId ?: return - holder.view.onClick { - callback?.onMapClicked() - } - GlideApp.with(holder.staticMapImageView) .load(location) .apply(RequestOptions.centerCropTransform()) From 83ed80e6d8cd4014a101ffd8091f71e355c64f64 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 29 Jan 2022 07:40:41 +0100 Subject: [PATCH 16/22] Rename fun for clarity --- .../api/session/room/model/message/MessageLocationContent.kt | 2 +- .../app/features/home/room/detail/RoomDetailFragment.kt | 4 ++-- .../detail/timeline/action/MessageActionsEpoxyController.kt | 2 +- .../home/room/detail/timeline/factory/MessageItemFactory.kt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt index bf51e7177b..c090487c58 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt @@ -63,5 +63,5 @@ data class MessageLocationContent( @Json(name = "org.matrix.msc1767.text") val text: String? = null ) : MessageContent { - fun getUri() = locationInfo?.geoUri ?: geoUri + fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 7e2bb835e0..8db3bc2fa8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -614,7 +614,7 @@ class RoomDetailFragment @Inject constructor( private fun handleShowLocationPreview(locationContent: MessageLocationContent, senderId: String) { // TODO Create a helper - val geoUri = locationContent.getUri() + val geoUri = locationContent.getBestGeoUri() val locationData = LocationData.create(geoUri) navigator .openLocationSharing( @@ -1948,7 +1948,7 @@ class RoomDetailFragment @Inject constructor( when (action.messageContent) { is MessageTextContent -> shareText(requireContext(), action.messageContent.body) is MessageLocationContent -> { - LocationData.create(action.messageContent.getUri())?.let { + LocationData.create(action.messageContent.getBestGeoUri())?.let { openLocation(requireActivity(), it.latitude, it.longitude) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 59e54f17ab..14f56a7846 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -79,7 +79,7 @@ class MessageActionsEpoxyController @Inject constructor( val bindingOptions = spanUtils.getBindingOptions(body) val locationUrl = state.timelineEvent()?.root?.getClearContent() ?.toModel(catchError = true) - ?.let { LocationData.create(it.getUri()) } + ?.let { LocationData.create(it.getBestGeoUri()) } ?.let { urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, 1200, 800) } bottomSheetMessagePreviewItem { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 0b0373a889..8f88be0683 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -200,7 +200,7 @@ class MessageItemFactory @Inject constructor( informationData: MessageInformationData, highlight: Boolean, attributes: AbsMessageItem.Attributes): MessageLocationItem? { - val geoUri = locationContent.getUri() + val geoUri = locationContent.getBestGeoUri() val locationData = LocationData.create(geoUri) val width = resources.displayMetrics.widthPixels - dimensionConverter.dpToPx(60) From 303a858423b7f77af1b9d1a21028adf5c3694ba9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 29 Jan 2022 07:58:55 +0100 Subject: [PATCH 17/22] Create an extension, improve the parsing algorithm, add robustness and unit test it --- .../home/room/detail/RoomDetailAction.kt | 2 +- .../home/room/detail/RoomDetailFragment.kt | 9 +-- .../home/room/detail/RoomDetailViewEvents.kt | 1 - .../home/room/detail/RoomDetailViewModel.kt | 1 - .../action/MessageActionsEpoxyController.kt | 4 +- .../timeline/factory/MessageItemFactory.kt | 7 +-- .../app/features/location/LocationData.kt | 61 ++++++++++--------- .../app/features/location/LocationDataTest.kt | 60 ++++++++++++++++++ 8 files changed, 100 insertions(+), 45 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/location/LocationDataTest.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 18d9994cd5..14c8e598f8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -20,7 +20,6 @@ import android.net.Uri import android.view.View import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.call.conference.ConferenceEvent -import im.vector.app.features.location.LocationData import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent @@ -90,6 +89,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class EnsureNativeWidgetAllowed(val widget: Widget, val userJustAccepted: Boolean, val grantedEvents: RoomDetailViewEvents) : RoomDetailAction() + data class UpdateJoinJitsiCallStatus(val conferenceEvent: ConferenceEvent) : RoomDetailAction() data class OpenOrCreateDm(val userId: String) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 8db3bc2fa8..b58a1d627e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -171,8 +171,8 @@ import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.invite.VectorInviteView -import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationSharingMode +import im.vector.app.features.location.toLocationData import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.notifications.NotificationDrawerManager @@ -613,15 +613,12 @@ class RoomDetailFragment @Inject constructor( } private fun handleShowLocationPreview(locationContent: MessageLocationContent, senderId: String) { - // TODO Create a helper - val geoUri = locationContent.getBestGeoUri() - val locationData = LocationData.create(geoUri) navigator .openLocationSharing( context = requireContext(), roomId = roomDetailArgs.roomId, mode = LocationSharingMode.PREVIEW, - initialLocationData = locationData, + initialLocationData = locationContent.toLocationData(), locationOwnerId = senderId ) } @@ -1948,7 +1945,7 @@ class RoomDetailFragment @Inject constructor( when (action.messageContent) { is MessageTextContent -> shareText(requireContext(), action.messageContent.body) is MessageLocationContent -> { - LocationData.create(action.messageContent.getBestGeoUri())?.let { + action.messageContent.toLocationData()?.let { openLocation(requireActivity(), it.latitude, it.longitude) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index ee823adc17..86240a5ffe 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -20,7 +20,6 @@ import android.net.Uri import android.view.View import im.vector.app.core.platform.VectorViewEvents import im.vector.app.features.call.webrtc.WebRtcCall -import im.vector.app.features.location.LocationData import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 1bb70e29d7..9149ae1dca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -53,7 +53,6 @@ import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandle import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.typing.TypingHelper -import im.vector.app.features.location.LocationData import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorDataStore diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 14f56a7846..086a093068 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -40,8 +40,8 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement import im.vector.app.features.home.room.detail.timeline.tools.linkify import im.vector.app.features.html.SpanUtils import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE -import im.vector.app.features.location.LocationData import im.vector.app.features.location.UrlMapProvider +import im.vector.app.features.location.toLocationData import im.vector.app.features.media.ImageContentRenderer import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse @@ -79,7 +79,7 @@ class MessageActionsEpoxyController @Inject constructor( val bindingOptions = spanUtils.getBindingOptions(body) val locationUrl = state.timelineEvent()?.root?.getClearContent() ?.toModel(catchError = true) - ?.let { LocationData.create(it.getBestGeoUri()) } + ?.toLocationData() ?.let { urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, 1200, 800) } bottomSheetMessagePreviewItem { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 8f88be0683..352b87a4d8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -72,8 +72,8 @@ import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.SpanUtils import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE -import im.vector.app.features.location.LocationData import im.vector.app.features.location.UrlMapProvider +import im.vector.app.features.location.toLocationData import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.settings.VectorPreferences @@ -200,13 +200,10 @@ class MessageItemFactory @Inject constructor( informationData: MessageInformationData, highlight: Boolean, attributes: AbsMessageItem.Attributes): MessageLocationItem? { - val geoUri = locationContent.getBestGeoUri() - val locationData = LocationData.create(geoUri) - val width = resources.displayMetrics.widthPixels - dimensionConverter.dpToPx(60) val height = dimensionConverter.dpToPx(200) - val locationUrl = locationData?.let { + val locationUrl = locationContent.toLocationData()?.let { urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height) } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationData.kt b/vector/src/main/java/im/vector/app/features/location/LocationData.kt index c3ff09ebcd..0a88fb52d9 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationData.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationData.kt @@ -17,41 +17,44 @@ package im.vector.app.features.location import android.os.Parcelable +import androidx.annotation.VisibleForTesting import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent @Parcelize data class LocationData( val latitude: Double, val longitude: Double, val uncertainty: Double? -) : Parcelable { +) : Parcelable - companion object { - - /** - * Creates location data from geo uri - * @param geoUri geo:latitude,longitude;uncertainty - * @return location data or null if geo uri is not valid - */ - fun create(geoUri: String): LocationData? { - val geoParts = geoUri - .split(":") - .takeIf { it.firstOrNull() == "geo" } - ?.getOrNull(1) - ?.split(",") - - val latitude = geoParts?.firstOrNull() - val geoTailParts = geoParts?.getOrNull(1)?.split(";") - val longitude = geoTailParts?.firstOrNull() - val uncertainty = geoTailParts?.getOrNull(1)?.replace("u=", "") - - return if (latitude != null && longitude != null) { - LocationData( - latitude = latitude.toDouble(), - longitude = longitude.toDouble(), - uncertainty = uncertainty?.toDouble() - ) - } else null - } - } +/** + * Creates location data from a LocationContent + * "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30) + * @return location data or null if geo uri is not valid + */ +fun MessageLocationContent.toLocationData(): LocationData? { + return parseGeo(getBestGeoUri()) } + +@VisibleForTesting +fun parseGeo(geo: String): LocationData? { + val geoParts = geo + .split(":") + .takeIf { it.firstOrNull() == "geo" } + ?.getOrNull(1) + ?.split(";") ?: return null + + val gpsParts = geoParts.getOrNull(0)?.split(",") ?: return null + val lat = gpsParts.getOrNull(0)?.toDoubleOrNull() ?: return null + val lng = gpsParts.getOrNull(1)?.toDoubleOrNull() ?: return null + + val uncertainty = geoParts.getOrNull(1)?.replace("u=", "")?.toDoubleOrNull() + + return LocationData( + latitude = lat, + longitude = lng, + uncertainty = uncertainty + ) +} + diff --git a/vector/src/test/java/im/vector/app/features/location/LocationDataTest.kt b/vector/src/test/java/im/vector/app/features/location/LocationDataTest.kt new file mode 100644 index 0000000000..fcfff0096f --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/location/LocationDataTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeNull +import org.junit.Test + +class LocationDataTest { + @Test + fun validCases() { + parseGeo("geo:12.34,56.78;13.56") shouldBeEqualTo + LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56) + parseGeo("geo:12.34,56.78") shouldBeEqualTo + LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null) + // Error is ignored in case of invalid uncertainty + parseGeo("geo:12.34,56.78;13.5z6") shouldBeEqualTo + LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null) + parseGeo("geo:12.34,56.78;13. 56") shouldBeEqualTo + LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null) + // Space are ignored (trim) + parseGeo("geo: 12.34,56.78;13.56") shouldBeEqualTo + LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56) + parseGeo("geo:12.34,56.78; 13.56") shouldBeEqualTo + LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56) + } + + @Test + fun invalidCases() { + parseGeo("").shouldBeNull() + parseGeo("geo").shouldBeNull() + parseGeo("geo:").shouldBeNull() + parseGeo("geo:12.34").shouldBeNull() + parseGeo("geo:12.34;13.56").shouldBeNull() + parseGeo("gea:12.34,56.78;13.56").shouldBeNull() + parseGeo("geo:12.x34,56.78;13.56").shouldBeNull() + parseGeo("geo:12.34,56.7y8;13.56").shouldBeNull() + // Spaces are not ignored if inside the numbers + parseGeo("geo:12.3 4,56.78;13.56").shouldBeNull() + parseGeo("geo:12.34,56.7 8;13.56").shouldBeNull() + // Or in the protocol part + parseGeo(" geo:12.34,56.78;13.56").shouldBeNull() + parseGeo("ge o:12.34,56.78;13.56").shouldBeNull() + parseGeo("geo :12.34,56.78;13.56").shouldBeNull() + } +} From 99f82d9691a5eff0e90e4479cf766abe6fc845d6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 29 Jan 2022 08:34:20 +0100 Subject: [PATCH 18/22] Avoid taking into account network location if we have gps location. --- .../app/features/location/LocationTracker.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index ff66baf5ca..d1f06c9d51 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -23,6 +23,7 @@ import android.location.LocationManager import androidx.annotation.RequiresPermission import androidx.core.content.getSystemService import androidx.core.location.LocationListenerCompat +import im.vector.app.BuildConfig import timber.log.Timber import javax.inject.Inject @@ -39,9 +40,12 @@ class LocationTracker @Inject constructor( private var callback: Callback? = null + private var hasGpsProviderLocation = false + @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) fun start(callback: Callback?) { Timber.d("## LocationTracker. start()") + hasGpsProviderLocation = false this.callback = callback if (locationManager == null) { @@ -82,7 +86,16 @@ class LocationTracker @Inject constructor( } override fun onLocationChanged(location: Location) { - Timber.d("## LocationTracker. onLocationChanged") + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.d("## LocationTracker. onLocationChanged: $location") + } else { + Timber.d("## LocationTracker. onLocationChanged") + } + if (location.provider != LocationManager.GPS_PROVIDER && hasGpsProviderLocation) { + // Ignore this update + Timber.d("## LocationTracker. ignoring location from ${location.provider}, we have gps location") + return + } callback?.onLocationUpdate(location.toLocationData()) } From a8c251f6f5a951c571b07d674bcedff4dee20a25 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 29 Jan 2022 08:51:30 +0100 Subject: [PATCH 19/22] Avoid taking into account any provider location if we have gps location. --- .../app/features/location/LocationTracker.kt | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index d1f06c9d51..bf59a08119 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -56,13 +56,19 @@ class LocationTracker @Inject constructor( locationManager.allProviders .takeIf { it.isNotEmpty() } + // Take GPS first + ?.sortedByDescending { if (it == LocationManager.GPS_PROVIDER) 1 else 0 } ?.forEach { provider -> Timber.d("## LocationTracker. track location using $provider") // Send last known location without waiting location updates locationManager.getLastKnownLocation(provider)?.let { lastKnownLocation -> - Timber.d("## LocationTracker. lastKnownLocation") - callback?.onLocationUpdate(lastKnownLocation.toLocationData()) + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.d("## LocationTracker. lastKnownLocation: $lastKnownLocation") + } else { + Timber.d("## LocationTracker. lastKnownLocation") + } + onLocationChanged(lastKnownLocation) } locationManager.requestLocationUpdates( @@ -91,10 +97,21 @@ class LocationTracker @Inject constructor( } else { Timber.d("## LocationTracker. onLocationChanged") } - if (location.provider != LocationManager.GPS_PROVIDER && hasGpsProviderLocation) { - // Ignore this update - Timber.d("## LocationTracker. ignoring location from ${location.provider}, we have gps location") - return + notifyLocation(location) + } + + private fun notifyLocation(location: Location) { + when (location.provider) { + LocationManager.GPS_PROVIDER -> { + hasGpsProviderLocation = true + } + else -> { + if (hasGpsProviderLocation) { + // Ignore this update + Timber.d("## LocationTracker. ignoring location from ${location.provider}, we have gps location") + return + } + } } callback?.onLocationUpdate(location.toLocationData()) } From 2fbb4340996ee94b71a4832051cd8f4c6cdce006 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 29 Jan 2022 08:52:20 +0100 Subject: [PATCH 20/22] Format --- .../main/java/im/vector/app/features/location/LocationData.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationData.kt b/vector/src/main/java/im/vector/app/features/location/LocationData.kt index 0a88fb52d9..a69d8d20e3 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationData.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationData.kt @@ -57,4 +57,3 @@ fun parseGeo(geo: String): LocationData? { uncertainty = uncertainty ) } - From ecd41d38260507eed54f370dcd429017cc1d31a3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 31 Jan 2022 12:47:17 +0100 Subject: [PATCH 21/22] network "not live" lcoation can be more accurate than GPS "not live" location. So do not ignore them. Not sure how if this is a universal rule... --- .../app/features/location/LocationTracker.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index bf59a08119..162fbc5959 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -40,12 +40,12 @@ class LocationTracker @Inject constructor( private var callback: Callback? = null - private var hasGpsProviderLocation = false + private var hasGpsProviderLiveLocation = false @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) fun start(callback: Callback?) { Timber.d("## LocationTracker. start()") - hasGpsProviderLocation = false + hasGpsProviderLiveLocation = false this.callback = callback if (locationManager == null) { @@ -66,9 +66,9 @@ class LocationTracker @Inject constructor( if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { Timber.d("## LocationTracker. lastKnownLocation: $lastKnownLocation") } else { - Timber.d("## LocationTracker. lastKnownLocation") + Timber.d("## LocationTracker. lastKnownLocation: ${lastKnownLocation.provider}") } - onLocationChanged(lastKnownLocation) + notifyLocation(lastKnownLocation, isLive = false) } locationManager.requestLocationUpdates( @@ -95,20 +95,20 @@ class LocationTracker @Inject constructor( if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { Timber.d("## LocationTracker. onLocationChanged: $location") } else { - Timber.d("## LocationTracker. onLocationChanged") + Timber.d("## LocationTracker. onLocationChanged: ${location.provider}") } - notifyLocation(location) + notifyLocation(location, isLive = true) } - private fun notifyLocation(location: Location) { + private fun notifyLocation(location: Location, isLive: Boolean) { when (location.provider) { LocationManager.GPS_PROVIDER -> { - hasGpsProviderLocation = true + hasGpsProviderLiveLocation = isLive } else -> { - if (hasGpsProviderLocation) { + if (hasGpsProviderLiveLocation) { // Ignore this update - Timber.d("## LocationTracker. ignoring location from ${location.provider}, we have gps location") + Timber.d("## LocationTracker. ignoring location from ${location.provider}, we have gps live location") return } } From 4e3c730940c3de05e49bbfde8276dd56ea082c1d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 31 Jan 2022 12:49:08 +0100 Subject: [PATCH 22/22] Changelog --- changelog.d/5084.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5084.bugfix diff --git a/changelog.d/5084.bugfix b/changelog.d/5084.bugfix new file mode 100644 index 0000000000..95a3a20cd1 --- /dev/null +++ b/changelog.d/5084.bugfix @@ -0,0 +1 @@ +Display static map images in the timeline and improve Location sharing feature \ No newline at end of file