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
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/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/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt
index cdecd2d6c6..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
@@ -17,24 +17,25 @@
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.LocationData
-import im.vector.app.features.location.MapTilerMapView
import im.vector.app.features.media.ImageContentRenderer
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
import org.matrix.android.sdk.api.util.MatrixItem
@@ -70,7 +71,7 @@ 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)
- }
- }
+ if (locationUrl == null) {
+ holder.body.isVisible = true
+ holder.mapViewContainer.isVisible = false
+ } else {
+ holder.body.isVisible = false
+ holder.mapViewContainer.isVisible = true
+ GlideApp.with(holder.staticMapImageView)
+ .load(locationUrl)
+ .apply(RequestOptions.centerCropTransform())
+ .into(holder.staticMapImageView)
+
+ locationPinProvider?.create(matrixItem.id) { pinDrawable ->
+ GlideApp.with(holder.staticMapPinImageView)
+ .load(pinDrawable)
+ .into(holder.staticMapPinImageView)
}
}
}
@@ -124,6 +129,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/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
index 58e36d2303..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()
@@ -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..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
@@ -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,14 @@ class RoomDetailFragment @Inject constructor(
}
}
- private fun handleShowLocationPreview(viewEvent: RoomDetailViewEvents.ShowLocation) {
+ private fun handleShowLocationPreview(locationContent: MessageLocationContent, senderId: String) {
navigator
.openLocationSharing(
context = requireContext(),
roomId = roomDetailArgs.roomId,
mode = LocationSharingMode.PREVIEW,
- initialLocationData = viewEvent.locationData,
- locationOwnerId = viewEvent.userId
+ initialLocationData = locationContent.toLocationData(),
+ locationOwnerId = senderId
)
}
@@ -1828,6 +1827,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")
+ }
}
}
@@ -1940,7 +1945,7 @@ class RoomDetailFragment @Inject constructor(
when (action.messageContent) {
is MessageTextContent -> shareText(requireContext(), action.messageContent.body)
is MessageLocationContent -> {
- LocationData.create(action.messageContent.getUri())?.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 b0921e01f9..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
@@ -83,6 +82,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..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
@@ -385,14 +384,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/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
index 1ff9679479..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
@@ -39,7 +39,9 @@ import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.app.features.home.room.detail.timeline.tools.linkify
import im.vector.app.features.html.SpanUtils
-import im.vector.app.features.location.LocationData
+import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE
+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
@@ -62,6 +64,7 @@ class MessageActionsEpoxyController @Inject constructor(
private val spanUtils: SpanUtils,
private val eventDetailsFormatter: EventDetailsFormatter,
private val dateFormatter: VectorDateFormatter,
+ private val urlMapProvider: UrlMapProvider,
private val locationPinProvider: LocationPinProvider
) : TypedEpoxyController() {
@@ -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)
+ ?.toLocationData()
+ ?.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 eab7621d14..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
@@ -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
@@ -33,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
@@ -71,7 +71,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.LocationData
+import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE
+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
@@ -127,7 +129,10 @@ 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 urlMapProvider: UrlMapProvider,
+ private val resources: Resources
+) {
// TODO inject this properly?
private var roomId: String = ""
@@ -182,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)
}
@@ -194,27 +199,21 @@ 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 width = resources.displayMetrics.widthPixels - dimensionConverter.dpToPx(60)
+ val height = dimensionConverter.dpToPx(200)
- val mapCallback: MessageLocationItem.Callback = object : MessageLocationItem.Callback {
- override fun onMapClicked() {
- locationData?.let {
- callback?.onTimelineItemAction(RoomDetailAction.ShowLocation(it, informationData.senderId))
- }
- }
+ val locationUrl = locationContent.toLocationData()?.let {
+ urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height)
}
return MessageLocationItem_()
.attributes(attributes)
- .locationData(locationData)
+ .locationUrl(locationUrl)
.userId(informationData.senderId)
.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/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/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..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
@@ -16,28 +16,19 @@
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.LocationData
-import im.vector.app.features.location.MapTilerMapView
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageLocationItem : AbsMessageItem() {
- interface Callback {
- fun onMapClicked()
- }
-
@EpoxyAttribute
- var callback: Callback? = null
-
- @EpoxyAttribute
- var locationData: LocationData? = null
+ var locationUrl: String? = null
@EpoxyAttribute
var userId: String? = null
@@ -47,37 +38,31 @@ 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 location = locationUrl ?: return
val locationOwnerId = userId ?: return
- holder.clickableMapArea.onClick {
- callback?.onMapClicked()
- }
+ GlideApp.with(holder.staticMapImageView)
+ .load(location)
+ .apply(RequestOptions.centerCropTransform())
+ .into(holder.staticMapImageView)
- holder.mapView.apply {
- initialize {
- zoomToLocation(location.latitude, location.longitude, INITIAL_ZOOM)
-
- locationPinProvider?.create(locationOwnerId) { pinDrawable ->
- addPinToMap(locationOwnerId, pinDrawable)
- updatePinLocation(locationOwnerId, location.latitude, location.longitude)
- }
- }
+ locationPinProvider?.create(locationOwnerId) { 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 staticMapImageView by bind(R.id.staticMapImageView)
+ val staticMapPinImageView by bind(R.id.staticMapPinImageView)
}
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/Config.kt b/vector/src/main/java/im/vector/app/features/location/Config.kt
index 630df16a37..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,6 +16,10 @@
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 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
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..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
@@ -17,41 +17,43 @@
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/main/java/im/vector/app/features/location/LocationPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt
index 6209bf5a4f..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
@@ -21,20 +21,30 @@ 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
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 urlMapProvider: UrlMapProvider,
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,11 +52,15 @@ class LocationPreviewFragment @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(urlMapProvider.mapUrl)
+ loadPinDrawable()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ views.mapView.onResume()
}
override fun onPause() {
@@ -54,11 +68,32 @@ class LocationPreviewFragment @Inject constructor(
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 {
@@ -76,18 +111,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)
- deleteAllPins()
- 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/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 900f465f04..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
@@ -20,29 +20,29 @@ 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
+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
- }
+ private val urlMapProvider: UrlMapProvider
+) : 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(urlMapProvider.mapUrl)
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)
@@ -118,9 +110,15 @@ class LocationSharingFragment @Inject constructor(
.setPositiveButton(R.string.ok) { _, _ ->
activity?.finish()
}
+ .setCancelable(false)
.show()
}
+ override fun invalidate() = withState(viewModel) { state ->
+ views.mapView.render(state.toMapState())
+ views.shareLocationGpsLoading.isGone = state.lastKnownLocation != null
+ }
+
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..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
@@ -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(
@@ -36,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/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt
index 0c0315cf34..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
@@ -19,70 +19,108 @@ 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 im.vector.app.BuildConfig
import timber.log.Timber
import javax.inject.Inject
class LocationTracker @Inject constructor(
- private val context: Context
-) : LocationListener {
+ 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
+
+ private var hasGpsProviderLiveLocation = false
@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()")
+ hasGpsProviderLiveLocation = false
+ 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
}
+
+ 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 ->
+ if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
+ Timber.d("## LocationTracker. lastKnownLocation: $lastKnownLocation")
+ } else {
+ Timber.d("## LocationTracker. lastKnownLocation: ${lastKnownLocation.provider}")
+ }
+ notifyLocation(lastKnownLocation, isLive = false)
+ }
+
+ 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])
fun stop() {
+ Timber.d("## LocationTracker. stop()")
locationManager?.removeUpdates(this)
callback = null
}
override fun onLocationChanged(location: Location) {
+ if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
+ Timber.d("## LocationTracker. onLocationChanged: $location")
+ } else {
+ Timber.d("## LocationTracker. onLocationChanged: ${location.provider}")
+ }
+ notifyLocation(location, isLive = true)
+ }
+
+ private fun notifyLocation(location: Location, isLive: Boolean) {
+ when (location.provider) {
+ LocationManager.GPS_PROVIDER -> {
+ hasGpsProviderLiveLocation = isLive
+ }
+ else -> {
+ if (hasGpsProviderLiveLocation) {
+ // Ignore this update
+ Timber.d("## LocationTracker. ignoring location from ${location.provider}, we have gps live location")
+ return
+ }
+ }
+ }
callback?.onLocationUpdate(location.toLocationData())
}
+ override fun onProviderDisabled(provider: String) {
+ Timber.d("## LocationTracker. onProviderDisabled: $provider")
+ callback?.onLocationProviderIsNotAvailable()
+ }
+
private fun Location.toLocationData(): LocationData {
return LocationData(latitude, longitude, accuracy.toDouble())
}
diff --git a/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapState.kt
similarity index 61%
rename from vector/src/main/java/im/vector/app/features/location/VectorMapView.kt
rename to vector/src/main/java/im/vector/app/features/location/MapState.kt
index 23b59bf99a..d001457e4f 100644
--- a/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt
+++ b/vector/src/main/java/im/vector/app/features/location/MapState.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 New Vector Ltd
+ * 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.
@@ -18,15 +18,9 @@ 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)
-}
+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 c64af1ebaa..dd80f701f6 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
@@ -27,65 +26,76 @@ 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
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: MapState? = 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
+
+ /**
+ * For location fragments
+ */
+ fun initialize(url: String) {
+ Timber.d("## Location: initialize")
getMapAsync { map ->
- map.setStyle(styleUrl) { style ->
- this.symbolManager = SymbolManager(this, map, style)
- this.map = map
- this.style = style
- onMapReady()
+ map.setStyle(url) { style ->
+ mapRefs = MapRefs(
+ map,
+ SymbolManager(this, map, style),
+ style
+ )
+ pendingState?.let { render(it) }
+ pendingState = null
}
}
}
- override fun addPinToMap(pinId: String, image: Drawable) {
- style?.addImage(pinId, image)
- }
+ fun render(state: MapState) {
+ val safeMapRefs = mapRefs ?: return Unit.also {
+ pendingState = state
+ }
- override fun updatePinLocation(pinId: String, latitude: Double, longitude: Double) {
- symbolManager?.create(
- SymbolOptions()
- .withLatLng(LatLng(latitude, longitude))
- .withIconImage(pinId)
- .withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
- )
- }
+ state.pinDrawable?.let { pinDrawable ->
+ if (!safeMapRefs.style.isFullyLoaded ||
+ safeMapRefs.style.getImage(state.pinId) == null) {
+ safeMapRefs.style.addImage(state.pinId, pinDrawable)
+ }
+ }
- override fun deleteAllPins() {
- symbolManager?.deleteAll()
- }
+ state.pinLocationData?.let { locationData ->
+ if (!initZoomDone || !state.zoomOnlyOnce) {
+ zoomToLocation(locationData.latitude, locationData.longitude)
+ initZoomDone = true
+ }
- override fun zoomToLocation(latitude: Double, longitude: Double, zoom: Double) {
- 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
+ safeMapRefs.symbolManager.deleteAll()
+ safeMapRefs.symbolManager.create(
+ SymbolOptions()
+ .withLatLng(LatLng(locationData.latitude, locationData.longitude))
+ .withIconImage(state.pinId)
+ .withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
+ )
}
}
- companion object {
- private const val styleUrl = "https://api.maptiler.com/maps/streets/style.json?key=${BuildConfig.mapTilerKey}"
+ private fun zoomToLocation(latitude: Double, longitude: Double) {
+ Timber.d("## Location: zoomToLocation")
+ mapRefs?.map?.cameraPosition = CameraPosition.Builder()
+ .target(LatLng(latitude, longitude))
+ .zoom(INITIAL_MAP_ZOOM_IN_PREVIEW)
+ .build()
}
}
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")
+ }
+ }
+ }
+}
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
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..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
@@ -103,18 +103,34 @@
tools:text="1080 x 1024 - 43s - 12kB"
tools:visibility="visible" />
-
+ tools:alpha="0.3"
+ tools:visibility="visible">
+
+
+
+
+
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..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,30 +6,19 @@
android:layout_height="wrap_content"
app:cardCornerRadius="8dp">
-
+ android:layout_height="200dp"
+ 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.
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()
+ }
+}