Merge pull request #5084 from vector-im/feature/bma/location_crash
Fix location crash
This commit is contained in:
commit
91e444ca73
1
changelog.d/5084.bugfix
Normal file
1
changelog.d/5084.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Display static map images in the timeline and improve Location sharing feature
|
6
library/ui-styles/src/main/res/values-ldrtl/bools.xml
Normal file
6
library/ui-styles/src/main/res/values-ldrtl/bools.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<bool name="is_rtl">true</bool>
|
||||||
|
|
||||||
|
</resources>
|
@ -4,4 +4,6 @@
|
|||||||
<!-- Created to detect what has to be implemented (especially in the settings) -->
|
<!-- Created to detect what has to be implemented (especially in the settings) -->
|
||||||
<bool name="false_not_implemented">false</bool>
|
<bool name="false_not_implemented">false</bool>
|
||||||
|
|
||||||
|
<bool name="is_rtl">false</bool>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -63,5 +63,5 @@ data class MessageLocationContent(
|
|||||||
@Json(name = "org.matrix.msc1767.text") val text: String? = null
|
@Json(name = "org.matrix.msc1767.text") val text: String? = null
|
||||||
) : MessageContent {
|
) : MessageContent {
|
||||||
|
|
||||||
fun getUri() = locationInfo?.geoUri ?: geoUri
|
fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri
|
||||||
}
|
}
|
||||||
|
@ -17,24 +17,25 @@
|
|||||||
package im.vector.app.core.epoxy.bottomsheet
|
package im.vector.app.core.epoxy.bottomsheet
|
||||||
|
|
||||||
import android.text.method.MovementMethod
|
import android.text.method.MovementMethod
|
||||||
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.epoxy.ClickListener
|
import im.vector.app.core.epoxy.ClickListener
|
||||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||||
import im.vector.app.core.epoxy.onClick
|
import im.vector.app.core.epoxy.onClick
|
||||||
import im.vector.app.core.extensions.setTextOrHide
|
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.displayname.getBestName
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
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.helper.LocationPinProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.BindingOptions
|
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.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.app.features.media.ImageContentRenderer
|
||||||
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
|
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
@ -70,7 +71,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
|
|||||||
var time: String? = null
|
var time: String? = null
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var locationData: LocationData? = null
|
var locationUrl: String? = null
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var locationPinProvider: LocationPinProvider? = null
|
var locationPinProvider: LocationPinProvider? = null
|
||||||
@ -97,17 +98,21 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
|
|||||||
body.charSequence.findPillsAndProcess(coroutineScope) { it.bind(holder.body) }
|
body.charSequence.findPillsAndProcess(coroutineScope) { it.bind(holder.body) }
|
||||||
holder.timestamp.setTextOrHide(time)
|
holder.timestamp.setTextOrHide(time)
|
||||||
|
|
||||||
holder.mapView.isVisible = locationData != null
|
if (locationUrl == null) {
|
||||||
holder.body.isVisible = locationData == null
|
holder.body.isVisible = true
|
||||||
locationData?.let { location ->
|
holder.mapViewContainer.isVisible = false
|
||||||
holder.mapView.initialize {
|
} else {
|
||||||
if (holder.view.isAttachedToWindow) {
|
holder.body.isVisible = false
|
||||||
holder.mapView.zoomToLocation(location.latitude, location.longitude, 15.0)
|
holder.mapViewContainer.isVisible = true
|
||||||
|
GlideApp.with(holder.staticMapImageView)
|
||||||
|
.load(locationUrl)
|
||||||
|
.apply(RequestOptions.centerCropTransform())
|
||||||
|
.into(holder.staticMapImageView)
|
||||||
|
|
||||||
locationPinProvider?.create(matrixItem.id) { pinDrawable ->
|
locationPinProvider?.create(matrixItem.id) { pinDrawable ->
|
||||||
holder.mapView.addPinToMap(matrixItem.id, pinDrawable)
|
GlideApp.with(holder.staticMapPinImageView)
|
||||||
holder.mapView.updatePinLocation(matrixItem.id, location.latitude, location.longitude)
|
.load(pinDrawable)
|
||||||
}
|
.into(holder.staticMapPinImageView)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,6 +129,8 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
|
|||||||
val bodyDetails by bind<TextView>(R.id.bottom_sheet_message_preview_body_details)
|
val bodyDetails by bind<TextView>(R.id.bottom_sheet_message_preview_body_details)
|
||||||
val timestamp by bind<TextView>(R.id.bottom_sheet_message_preview_timestamp)
|
val timestamp by bind<TextView>(R.id.bottom_sheet_message_preview_timestamp)
|
||||||
val imagePreview by bind<ImageView>(R.id.bottom_sheet_message_preview_image)
|
val imagePreview by bind<ImageView>(R.id.bottom_sheet_message_preview_image)
|
||||||
val mapView by bind<MapTilerMapView>(R.id.bottom_sheet_message_preview_location)
|
val mapViewContainer by bind<FrameLayout>(R.id.mapViewContainer)
|
||||||
|
val staticMapImageView by bind<ImageView>(R.id.staticMapImageView)
|
||||||
|
val staticMapPinImageView by bind<ImageView>(R.id.staticMapPinImageView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ import android.net.Uri
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
import im.vector.app.features.call.conference.ConferenceEvent
|
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.content.ContentAttachmentData
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
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,
|
data class EnsureNativeWidgetAllowed(val widget: Widget,
|
||||||
val userJustAccepted: Boolean,
|
val userJustAccepted: Boolean,
|
||||||
val grantedEvents: RoomDetailViewEvents) : RoomDetailAction()
|
val grantedEvents: RoomDetailViewEvents) : RoomDetailAction()
|
||||||
|
|
||||||
data class UpdateJoinJitsiCallStatus(val conferenceEvent: ConferenceEvent) : RoomDetailAction()
|
data class UpdateJoinJitsiCallStatus(val conferenceEvent: ConferenceEvent) : RoomDetailAction()
|
||||||
|
|
||||||
data class OpenOrCreateDm(val userId: String) : RoomDetailAction()
|
data class OpenOrCreateDm(val userId: String) : RoomDetailAction()
|
||||||
@ -112,7 +112,4 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||||||
|
|
||||||
// Poll
|
// Poll
|
||||||
data class EndPoll(val eventId: String) : RoomDetailAction()
|
data class EndPoll(val eventId: String) : RoomDetailAction()
|
||||||
|
|
||||||
// Location
|
|
||||||
data class ShowLocation(val locationData: LocationData, val userId: String) : RoomDetailAction()
|
|
||||||
}
|
}
|
||||||
|
@ -171,8 +171,8 @@ import im.vector.app.features.html.EventHtmlRenderer
|
|||||||
import im.vector.app.features.html.PillImageSpan
|
import im.vector.app.features.html.PillImageSpan
|
||||||
import im.vector.app.features.html.PillsPostProcessor
|
import im.vector.app.features.html.PillsPostProcessor
|
||||||
import im.vector.app.features.invite.VectorInviteView
|
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.LocationSharingMode
|
||||||
|
import im.vector.app.features.location.toLocationData
|
||||||
import im.vector.app.features.media.ImageContentRenderer
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
import im.vector.app.features.media.VideoContentRenderer
|
import im.vector.app.features.media.VideoContentRenderer
|
||||||
import im.vector.app.features.notifications.NotificationDrawerManager
|
import im.vector.app.features.notifications.NotificationDrawerManager
|
||||||
@ -481,7 +481,6 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
|
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
|
||||||
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
|
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
|
||||||
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
|
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
|
||||||
is RoomDetailViewEvents.ShowLocation -> handleShowLocationPreview(it)
|
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -613,14 +612,14 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShowLocationPreview(viewEvent: RoomDetailViewEvents.ShowLocation) {
|
private fun handleShowLocationPreview(locationContent: MessageLocationContent, senderId: String) {
|
||||||
navigator
|
navigator
|
||||||
.openLocationSharing(
|
.openLocationSharing(
|
||||||
context = requireContext(),
|
context = requireContext(),
|
||||||
roomId = roomDetailArgs.roomId,
|
roomId = roomDetailArgs.roomId,
|
||||||
mode = LocationSharingMode.PREVIEW,
|
mode = LocationSharingMode.PREVIEW,
|
||||||
initialLocationData = viewEvent.locationData,
|
initialLocationData = locationContent.toLocationData(),
|
||||||
locationOwnerId = viewEvent.userId
|
locationOwnerId = senderId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1828,6 +1827,12 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
is EncryptedEventContent -> {
|
is EncryptedEventContent -> {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId))
|
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) {
|
when (action.messageContent) {
|
||||||
is MessageTextContent -> shareText(requireContext(), action.messageContent.body)
|
is MessageTextContent -> shareText(requireContext(), action.messageContent.body)
|
||||||
is MessageLocationContent -> {
|
is MessageLocationContent -> {
|
||||||
LocationData.create(action.messageContent.getUri())?.let {
|
action.messageContent.toLocationData()?.let {
|
||||||
openLocation(requireActivity(), it.latitude, it.longitude)
|
openLocation(requireActivity(), it.latitude, it.longitude)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ import android.net.Uri
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
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.session.widgets.model.Widget
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
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()
|
data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents()
|
||||||
object StopChatEffects : RoomDetailViewEvents()
|
object StopChatEffects : RoomDetailViewEvents()
|
||||||
object RoomReplacementStarted : RoomDetailViewEvents()
|
object RoomReplacementStarted : RoomDetailViewEvents()
|
||||||
|
|
||||||
data class ShowLocation(val locationData: LocationData, val userId: String) : RoomDetailViewEvents()
|
|
||||||
}
|
}
|
||||||
|
@ -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.factory.TimelineFactory
|
||||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
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.home.room.typing.TypingHelper
|
||||||
import im.vector.app.features.location.LocationData
|
|
||||||
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
||||||
import im.vector.app.features.session.coroutineScope
|
import im.vector.app.features.session.coroutineScope
|
||||||
import im.vector.app.features.settings.VectorDataStore
|
import im.vector.app.features.settings.VectorDataStore
|
||||||
@ -385,14 +384,9 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
_viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true))
|
_viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true))
|
||||||
}
|
}
|
||||||
is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId)
|
is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId)
|
||||||
is RoomDetailAction.ShowLocation -> handleShowLocation(action.locationData, action.userId)
|
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShowLocation(locationData: LocationData, userId: String) {
|
|
||||||
_viewEvents.post(RoomDetailViewEvents.ShowLocation(locationData, userId))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleJitsiCallJoinStatus(action: RoomDetailAction.UpdateJoinJitsiCallStatus) = withState { state ->
|
private fun handleJitsiCallJoinStatus(action: RoomDetailAction.UpdateJoinJitsiCallStatus) = withState { state ->
|
||||||
if (state.jitsiState.confId == null) {
|
if (state.jitsiState.confId == null) {
|
||||||
// If jitsi widget is removed while on the call
|
// If jitsi widget is removed while on the call
|
||||||
|
@ -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.createLinkMovementMethod
|
||||||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||||
import im.vector.app.features.html.SpanUtils
|
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.app.features.media.ImageContentRenderer
|
||||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
@ -62,6 +64,7 @@ class MessageActionsEpoxyController @Inject constructor(
|
|||||||
private val spanUtils: SpanUtils,
|
private val spanUtils: SpanUtils,
|
||||||
private val eventDetailsFormatter: EventDetailsFormatter,
|
private val eventDetailsFormatter: EventDetailsFormatter,
|
||||||
private val dateFormatter: VectorDateFormatter,
|
private val dateFormatter: VectorDateFormatter,
|
||||||
|
private val urlMapProvider: UrlMapProvider,
|
||||||
private val locationPinProvider: LocationPinProvider
|
private val locationPinProvider: LocationPinProvider
|
||||||
) : TypedEpoxyController<MessageActionState>() {
|
) : TypedEpoxyController<MessageActionState>() {
|
||||||
|
|
||||||
@ -74,9 +77,11 @@ class MessageActionsEpoxyController @Inject constructor(
|
|||||||
val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL)
|
val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL)
|
||||||
val body = state.messageBody.linkify(host.listener)
|
val body = state.messageBody.linkify(host.listener)
|
||||||
val bindingOptions = spanUtils.getBindingOptions(body)
|
val bindingOptions = spanUtils.getBindingOptions(body)
|
||||||
val locationData = state.timelineEvent()?.root?.getClearContent()?.toModel<MessageLocationContent>(catchError = true)?.let {
|
val locationUrl = state.timelineEvent()?.root?.getClearContent()
|
||||||
LocationData.create(it.getUri())
|
?.toModel<MessageLocationContent>(catchError = true)
|
||||||
}
|
?.toLocationData()
|
||||||
|
?.let { urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, 1200, 800) }
|
||||||
|
|
||||||
bottomSheetMessagePreviewItem {
|
bottomSheetMessagePreviewItem {
|
||||||
id("preview")
|
id("preview")
|
||||||
avatarRenderer(host.avatarRenderer)
|
avatarRenderer(host.avatarRenderer)
|
||||||
@ -89,7 +94,7 @@ class MessageActionsEpoxyController @Inject constructor(
|
|||||||
body(body.toEpoxyCharSequence())
|
body(body.toEpoxyCharSequence())
|
||||||
bodyDetails(host.eventDetailsFormatter.format(state.timelineEvent()?.root)?.toEpoxyCharSequence())
|
bodyDetails(host.eventDetailsFormatter.format(state.timelineEvent()?.root)?.toEpoxyCharSequence())
|
||||||
time(formattedDate)
|
time(formattedDate)
|
||||||
locationData(locationData)
|
locationUrl(locationUrl)
|
||||||
locationPinProvider(host.locationPinProvider)
|
locationPinProvider(host.locationPinProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.factory
|
package im.vector.app.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
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.resources.StringProvider
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
import im.vector.app.core.utils.containsOnlyEmojis
|
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.TimelineEventController
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
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.PillsPostProcessor
|
||||||
import im.vector.app.features.html.SpanUtils
|
import im.vector.app.features.html.SpanUtils
|
||||||
import im.vector.app.features.html.VectorHtmlCompressor
|
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.ImageContentRenderer
|
||||||
import im.vector.app.features.media.VideoContentRenderer
|
import im.vector.app.features.media.VideoContentRenderer
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
@ -127,7 +129,10 @@ class MessageItemFactory @Inject constructor(
|
|||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
|
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
|
||||||
private val locationPinProvider: LocationPinProvider,
|
private val locationPinProvider: LocationPinProvider,
|
||||||
private val vectorPreferences: VectorPreferences) {
|
private val vectorPreferences: VectorPreferences,
|
||||||
|
private val urlMapProvider: UrlMapProvider,
|
||||||
|
private val resources: Resources
|
||||||
|
) {
|
||||||
|
|
||||||
// TODO inject this properly?
|
// TODO inject this properly?
|
||||||
private var roomId: String = ""
|
private var roomId: String = ""
|
||||||
@ -182,7 +187,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
|
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageLocationContent -> {
|
is MessageLocationContent -> {
|
||||||
if (vectorPreferences.labsRenderLocationsInTimeline()) {
|
if (vectorPreferences.labsRenderLocationsInTimeline()) {
|
||||||
buildLocationItem(messageContent, informationData, highlight, callback, attributes)
|
buildLocationItem(messageContent, informationData, highlight, attributes)
|
||||||
} else {
|
} else {
|
||||||
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
|
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
@ -194,27 +199,21 @@ class MessageItemFactory @Inject constructor(
|
|||||||
private fun buildLocationItem(locationContent: MessageLocationContent,
|
private fun buildLocationItem(locationContent: MessageLocationContent,
|
||||||
informationData: MessageInformationData,
|
informationData: MessageInformationData,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
|
||||||
attributes: AbsMessageItem.Attributes): MessageLocationItem? {
|
attributes: AbsMessageItem.Attributes): MessageLocationItem? {
|
||||||
val geoUri = locationContent.getUri()
|
val width = resources.displayMetrics.widthPixels - dimensionConverter.dpToPx(60)
|
||||||
val locationData = LocationData.create(geoUri)
|
val height = dimensionConverter.dpToPx(200)
|
||||||
|
|
||||||
val mapCallback: MessageLocationItem.Callback = object : MessageLocationItem.Callback {
|
val locationUrl = locationContent.toLocationData()?.let {
|
||||||
override fun onMapClicked() {
|
urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height)
|
||||||
locationData?.let {
|
|
||||||
callback?.onTimelineItemAction(RoomDetailAction.ShowLocation(it, informationData.senderId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return MessageLocationItem_()
|
return MessageLocationItem_()
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.locationData(locationData)
|
.locationUrl(locationUrl)
|
||||||
.userId(informationData.senderId)
|
.userId(informationData.senderId)
|
||||||
.locationPinProvider(locationPinProvider)
|
.locationPinProvider(locationPinProvider)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.callback(mapCallback)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildPollItem(pollContent: MessagePollContent,
|
private fun buildPollItem(pollContent: MessagePollContent,
|
||||||
|
@ -28,6 +28,7 @@ import im.vector.app.core.glide.GlideApp
|
|||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@ -54,22 +55,36 @@ class LocationPinProvider @Inject constructor(
|
|||||||
val size = dimensionConverter.dpToPx(44)
|
val size = dimensionConverter.dpToPx(44)
|
||||||
avatarRenderer.render(glideRequests, it, object : CustomTarget<Drawable>(size, size) {
|
avatarRenderer.render(glideRequests, it, object : CustomTarget<Drawable>(size, size) {
|
||||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||||
val bgUserPin = ContextCompat.getDrawable(context, R.drawable.bg_map_user_pin)!!
|
Timber.d("## Location: onResourceReady")
|
||||||
val layerDrawable = LayerDrawable(arrayOf(bgUserPin, resource))
|
val pinDrawable = createPinDrawable(resource)
|
||||||
val horizontalInset = dimensionConverter.dpToPx(4)
|
cache[userId] = pinDrawable
|
||||||
val topInset = dimensionConverter.dpToPx(4)
|
callback(pinDrawable)
|
||||||
val bottomInset = dimensionConverter.dpToPx(8)
|
|
||||||
layerDrawable.setLayerInset(1, horizontalInset, topInset, horizontalInset, bottomInset)
|
|
||||||
|
|
||||||
cache[userId] = layerDrawable
|
|
||||||
|
|
||||||
callback(layerDrawable)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadCleared(placeholder: Drawable?) {
|
override fun onLoadCleared(placeholder: Drawable?) {
|
||||||
// Is it possible? Put placeholder instead?
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,28 +16,19 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.item
|
package im.vector.app.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.ImageView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import im.vector.app.R
|
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.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)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>() {
|
abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>() {
|
||||||
|
|
||||||
interface Callback {
|
|
||||||
fun onMapClicked()
|
|
||||||
}
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var callback: Callback? = null
|
var locationUrl: String? = null
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
var locationData: LocationData? = null
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var userId: String? = null
|
var userId: String? = null
|
||||||
@ -47,37 +38,31 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
|
|||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
renderSendState(holder.mapViewContainer, null)
|
renderSendState(holder.view, null)
|
||||||
|
|
||||||
val location = locationData ?: return
|
val location = locationUrl ?: return
|
||||||
val locationOwnerId = userId ?: return
|
val locationOwnerId = userId ?: return
|
||||||
|
|
||||||
holder.clickableMapArea.onClick {
|
GlideApp.with(holder.staticMapImageView)
|
||||||
callback?.onMapClicked()
|
.load(location)
|
||||||
}
|
.apply(RequestOptions.centerCropTransform())
|
||||||
|
.into(holder.staticMapImageView)
|
||||||
holder.mapView.apply {
|
|
||||||
initialize {
|
|
||||||
zoomToLocation(location.latitude, location.longitude, INITIAL_ZOOM)
|
|
||||||
|
|
||||||
locationPinProvider?.create(locationOwnerId) { pinDrawable ->
|
locationPinProvider?.create(locationOwnerId) { pinDrawable ->
|
||||||
addPinToMap(locationOwnerId, pinDrawable)
|
GlideApp.with(holder.staticMapPinImageView)
|
||||||
updatePinLocation(locationOwnerId, location.latitude, location.longitude)
|
.load(pinDrawable)
|
||||||
}
|
.into(holder.staticMapPinImageView)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewType() = STUB_ID
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||||
val mapViewContainer by bind<ConstraintLayout>(R.id.mapViewContainer)
|
val staticMapImageView by bind<ImageView>(R.id.staticMapImageView)
|
||||||
val mapView by bind<MapTilerMapView>(R.id.mapView)
|
val staticMapPinImageView by bind<ImageView>(R.id.staticMapPinImageView)
|
||||||
val clickableMapArea by bind<FrameLayout>(R.id.clickableMapArea)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val STUB_ID = R.id.messageContentLocationStub
|
private const val STUB_ID = R.id.messageContentLocationStub
|
||||||
private const val INITIAL_ZOOM = 15.0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,10 @@
|
|||||||
|
|
||||||
package im.vector.app.features.location
|
package im.vector.app.features.location
|
||||||
|
|
||||||
const val INITIAL_MAP_ZOOM = 15.0
|
const val MAP_BASE_URL = "https://api.maptiler.com/maps/streets/style.json"
|
||||||
const val MIN_TIME_MILLIS_TO_UPDATE_LOCATION = 1 * 60 * 1000L // every 1 minute
|
const val STATIC_MAP_BASE_URL = "https://api.maptiler.com/maps/basic/static/"
|
||||||
const val MIN_DISTANCE_METERS_TO_UPDATE_LOCATION = 10f
|
|
||||||
|
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
|
||||||
|
@ -17,41 +17,43 @@
|
|||||||
package im.vector.app.features.location
|
package im.vector.app.features.location
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class LocationData(
|
data class LocationData(
|
||||||
val latitude: Double,
|
val latitude: Double,
|
||||||
val longitude: Double,
|
val longitude: Double,
|
||||||
val uncertainty: Double?
|
val uncertainty: Double?
|
||||||
) : Parcelable {
|
) : Parcelable
|
||||||
|
|
||||||
companion object {
|
/**
|
||||||
|
* Creates location data from a LocationContent
|
||||||
/**
|
* "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30)
|
||||||
* Creates location data from geo uri
|
|
||||||
* @param geoUri geo:latitude,longitude;uncertainty
|
|
||||||
* @return location data or null if geo uri is not valid
|
* @return location data or null if geo uri is not valid
|
||||||
*/
|
*/
|
||||||
fun create(geoUri: String): LocationData? {
|
fun MessageLocationContent.toLocationData(): LocationData? {
|
||||||
val geoParts = geoUri
|
return parseGeo(getBestGeoUri())
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun parseGeo(geo: String): LocationData? {
|
||||||
|
val geoParts = geo
|
||||||
.split(":")
|
.split(":")
|
||||||
.takeIf { it.firstOrNull() == "geo" }
|
.takeIf { it.firstOrNull() == "geo" }
|
||||||
?.getOrNull(1)
|
?.getOrNull(1)
|
||||||
?.split(",")
|
?.split(";") ?: return null
|
||||||
|
|
||||||
val latitude = geoParts?.firstOrNull()
|
val gpsParts = geoParts.getOrNull(0)?.split(",") ?: return null
|
||||||
val geoTailParts = geoParts?.getOrNull(1)?.split(";")
|
val lat = gpsParts.getOrNull(0)?.toDoubleOrNull() ?: return null
|
||||||
val longitude = geoTailParts?.firstOrNull()
|
val lng = gpsParts.getOrNull(1)?.toDoubleOrNull() ?: return null
|
||||||
val uncertainty = geoTailParts?.getOrNull(1)?.replace("u=", "")
|
|
||||||
|
|
||||||
return if (latitude != null && longitude != null) {
|
val uncertainty = geoParts.getOrNull(1)?.replace("u=", "")?.toDoubleOrNull()
|
||||||
LocationData(
|
|
||||||
latitude = latitude.toDouble(),
|
return LocationData(
|
||||||
longitude = longitude.toDouble(),
|
latitude = lat,
|
||||||
uncertainty = uncertainty?.toDouble()
|
longitude = lng,
|
||||||
|
uncertainty = uncertainty
|
||||||
)
|
)
|
||||||
} else null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -21,20 +21,30 @@ import android.view.LayoutInflater
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.airbnb.mvrx.args
|
import com.airbnb.mvrx.args
|
||||||
|
import com.mapbox.mapboxsdk.maps.MapView
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.core.utils.openLocation
|
import im.vector.app.core.utils.openLocation
|
||||||
import im.vector.app.databinding.FragmentLocationPreviewBinding
|
import im.vector.app.databinding.FragmentLocationPreviewBinding
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
|
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO Move locationPinProvider to a ViewModel
|
||||||
|
*/
|
||||||
class LocationPreviewFragment @Inject constructor(
|
class LocationPreviewFragment @Inject constructor(
|
||||||
|
private val urlMapProvider: UrlMapProvider,
|
||||||
private val locationPinProvider: LocationPinProvider
|
private val locationPinProvider: LocationPinProvider
|
||||||
) : VectorBaseFragment<FragmentLocationPreviewBinding>() {
|
) : VectorBaseFragment<FragmentLocationPreviewBinding>() {
|
||||||
|
|
||||||
private val args: LocationSharingArgs by args()
|
private val args: LocationSharingArgs by args()
|
||||||
|
|
||||||
|
// Keep a ref to handle properly the onDestroy callback
|
||||||
|
private var mapView: WeakReference<MapView>? = null
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationPreviewBinding {
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationPreviewBinding {
|
||||||
return FragmentLocationPreviewBinding.inflate(layoutInflater, container, false)
|
return FragmentLocationPreviewBinding.inflate(layoutInflater, container, false)
|
||||||
}
|
}
|
||||||
@ -42,11 +52,15 @@ class LocationPreviewFragment @Inject constructor(
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
views.mapView.initialize {
|
mapView = WeakReference(views.mapView)
|
||||||
if (isAdded) {
|
views.mapView.onCreate(savedInstanceState)
|
||||||
onMapReady()
|
views.mapView.initialize(urlMapProvider.mapUrl)
|
||||||
}
|
loadPinDrawable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
views.mapView.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@ -54,11 +68,32 @@ class LocationPreviewFragment @Inject constructor(
|
|||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLowMemory() {
|
||||||
|
views.mapView.onLowMemory()
|
||||||
|
super.onLowMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
views.mapView.onStart()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
views.mapView.onStop()
|
views.mapView.onStop()
|
||||||
super.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 getMenuRes() = R.menu.menu_location_preview
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
@ -76,18 +111,20 @@ class LocationPreviewFragment @Inject constructor(
|
|||||||
openLocation(requireActivity(), location.latitude, location.longitude)
|
openLocation(requireActivity(), location.latitude, location.longitude)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onMapReady() {
|
private fun loadPinDrawable() {
|
||||||
if (!isAdded) return
|
|
||||||
|
|
||||||
val location = args.initialLocationData ?: return
|
val location = args.initialLocationData ?: return
|
||||||
val userId = args.locationOwnerId
|
val userId = args.locationOwnerId
|
||||||
|
|
||||||
locationPinProvider.create(userId) { pinDrawable ->
|
locationPinProvider.create(userId) { pinDrawable ->
|
||||||
views.mapView.apply {
|
lifecycleScope.launchWhenResumed {
|
||||||
zoomToLocation(location.latitude, location.longitude, INITIAL_MAP_ZOOM)
|
views.mapView.render(
|
||||||
deleteAllPins()
|
MapState(
|
||||||
addPinToMap(userId, pinDrawable)
|
zoomOnlyOnce = true,
|
||||||
updatePinLocation(userId, location.latitude, location.longitude)
|
pinLocationData = location,
|
||||||
|
pinId = args.locationOwnerId,
|
||||||
|
pinDrawable = pinDrawable
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,5 @@ package im.vector.app.features.location
|
|||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class LocationSharingAction : VectorViewModelAction {
|
sealed class LocationSharingAction : VectorViewModelAction {
|
||||||
data class OnLocationUpdate(val locationData: LocationData) : LocationSharingAction()
|
|
||||||
object OnShareLocation : LocationSharingAction()
|
object OnShareLocation : LocationSharingAction()
|
||||||
object OnLocationProviderIsNotAvailable : LocationSharingAction()
|
|
||||||
}
|
}
|
||||||
|
@ -20,29 +20,29 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isGone
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.mapbox.mapboxsdk.maps.MapView
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.databinding.FragmentLocationSharingBinding
|
import im.vector.app.databinding.FragmentLocationSharingBinding
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
|
import java.lang.ref.WeakReference
|
||||||
import org.matrix.android.sdk.api.session.Session
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We should consider using SupportMapFragment for a out of the box lifecycle handling
|
||||||
|
*/
|
||||||
class LocationSharingFragment @Inject constructor(
|
class LocationSharingFragment @Inject constructor(
|
||||||
private val locationTracker: LocationTracker,
|
private val urlMapProvider: UrlMapProvider
|
||||||
private val session: Session,
|
) : VectorBaseFragment<FragmentLocationSharingBinding>() {
|
||||||
private val locationPinProvider: LocationPinProvider
|
|
||||||
) : VectorBaseFragment<FragmentLocationSharingBinding>(), LocationTracker.Callback {
|
|
||||||
|
|
||||||
init {
|
|
||||||
locationTracker.callback = this
|
|
||||||
}
|
|
||||||
|
|
||||||
private val viewModel: LocationSharingViewModel by fragmentViewModel()
|
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<MapView>? = null
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationSharingBinding {
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationSharingBinding {
|
||||||
return FragmentLocationSharingBinding.inflate(inflater, container, false)
|
return FragmentLocationSharingBinding.inflate(inflater, container, false)
|
||||||
@ -51,11 +51,9 @@ class LocationSharingFragment @Inject constructor(
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
views.mapView.initialize {
|
mapView = WeakReference(views.mapView)
|
||||||
if (isAdded) {
|
views.mapView.onCreate(savedInstanceState)
|
||||||
onMapReady()
|
views.mapView.initialize(urlMapProvider.mapUrl)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
views.shareLocationContainer.debouncedClicks {
|
views.shareLocationContainer.debouncedClicks {
|
||||||
viewModel.handle(LocationSharingAction.OnShareLocation)
|
viewModel.handle(LocationSharingAction.OnShareLocation)
|
||||||
@ -69,48 +67,42 @@ class LocationSharingFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
views.mapView.onResume()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
views.mapView.onPause()
|
views.mapView.onPause()
|
||||||
super.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() {
|
override fun onStop() {
|
||||||
views.mapView.onStop()
|
views.mapView.onStop()
|
||||||
super.onStop()
|
super.onStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLowMemory() {
|
||||||
|
super.onLowMemory()
|
||||||
|
views.mapView.onLowMemory()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
locationTracker.stop()
|
mapView?.get()?.onDestroy()
|
||||||
|
mapView?.clear()
|
||||||
super.onDestroy()
|
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() {
|
private fun handleLocationNotAvailableError() {
|
||||||
MaterialAlertDialogBuilder(requireActivity())
|
MaterialAlertDialogBuilder(requireActivity())
|
||||||
.setTitle(R.string.location_not_available_dialog_title)
|
.setTitle(R.string.location_not_available_dialog_title)
|
||||||
@ -118,9 +110,15 @@ class LocationSharingFragment @Inject constructor(
|
|||||||
.setPositiveButton(R.string.ok) { _, _ ->
|
.setPositiveButton(R.string.ok) { _, _ ->
|
||||||
activity?.finish()
|
activity?.finish()
|
||||||
}
|
}
|
||||||
|
.setCancelable(false)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
views.mapView.render(state.toMapState())
|
||||||
|
views.shareLocationGpsLoading.isGone = state.lastKnownLocation != null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val USER_PIN_NAME = "USER_PIN_NAME"
|
const val USER_PIN_NAME = "USER_PIN_NAME"
|
||||||
}
|
}
|
||||||
|
@ -24,12 +24,15 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
|||||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
|
||||||
class LocationSharingViewModel @AssistedInject constructor(
|
class LocationSharingViewModel @AssistedInject constructor(
|
||||||
@Assisted private val initialState: LocationSharingViewState,
|
@Assisted private val initialState: LocationSharingViewState,
|
||||||
session: Session
|
private val locationTracker: LocationTracker,
|
||||||
) : VectorViewModel<LocationSharingViewState, LocationSharingAction, LocationSharingViewEvents>(initialState) {
|
private val locationPinProvider: LocationPinProvider,
|
||||||
|
private val session: Session
|
||||||
|
) : VectorViewModel<LocationSharingViewState, LocationSharingAction, LocationSharingViewEvents>(initialState), LocationTracker.Callback {
|
||||||
|
|
||||||
private val room = session.getRoom(initialState.roomId)!!
|
private val room = session.getRoom(initialState.roomId)!!
|
||||||
|
|
||||||
@ -38,14 +41,31 @@ class LocationSharingViewModel @AssistedInject constructor(
|
|||||||
override fun create(initialState: LocationSharingViewState): LocationSharingViewModel
|
override fun create(initialState: LocationSharingViewState): LocationSharingViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : MavericksViewModelFactory<LocationSharingViewModel, LocationSharingViewState> by hiltMavericksViewModelFactory() {
|
companion object : MavericksViewModelFactory<LocationSharingViewModel, LocationSharingViewState> 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) {
|
override fun handle(action: LocationSharingAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is LocationSharingAction.OnLocationUpdate -> handleLocationUpdate(action.locationData)
|
|
||||||
LocationSharingAction.OnShareLocation -> handleShareLocation()
|
LocationSharingAction.OnShareLocation -> handleShareLocation()
|
||||||
LocationSharingAction.OnLocationProviderIsNotAvailable -> handleLocationProviderIsNotAvailable()
|
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,13 +82,13 @@ class LocationSharingViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLocationUpdate(locationData: LocationData) {
|
override fun onLocationUpdate(locationData: LocationData) {
|
||||||
setState {
|
setState {
|
||||||
copy(lastKnownLocation = locationData)
|
copy(lastKnownLocation = locationData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLocationProviderIsNotAvailable() {
|
override fun onLocationProviderIsNotAvailable() {
|
||||||
_viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError)
|
_viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.location
|
package im.vector.app.features.location
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
@ -28,7 +29,8 @@ enum class LocationSharingMode(@StringRes val titleRes: Int) {
|
|||||||
data class LocationSharingViewState(
|
data class LocationSharingViewState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val mode: LocationSharingMode,
|
val mode: LocationSharingMode,
|
||||||
val lastKnownLocation: LocationData? = null
|
val lastKnownLocation: LocationData? = null,
|
||||||
|
val pinDrawable: Drawable? = null
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
constructor(locationSharingArgs: LocationSharingArgs) : this(
|
constructor(locationSharingArgs: LocationSharingArgs) : this(
|
||||||
@ -36,3 +38,10 @@ data class LocationSharingViewState(
|
|||||||
mode = locationSharingArgs.mode
|
mode = locationSharingArgs.mode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun LocationSharingViewState.toMapState() = MapState(
|
||||||
|
zoomOnlyOnce = true,
|
||||||
|
pinLocationData = lastKnownLocation,
|
||||||
|
pinId = LocationSharingFragment.USER_PIN_NAME,
|
||||||
|
pinDrawable = pinDrawable
|
||||||
|
)
|
||||||
|
@ -19,70 +19,108 @@ package im.vector.app.features.location
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationListener
|
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.core.location.LocationListenerCompat
|
||||||
|
import im.vector.app.BuildConfig
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LocationTracker @Inject constructor(
|
class LocationTracker @Inject constructor(
|
||||||
private val context: Context
|
context: Context
|
||||||
) : LocationListener {
|
) : LocationListenerCompat {
|
||||||
|
|
||||||
|
private val locationManager = context.getSystemService<LocationManager>()
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onLocationUpdate(locationData: LocationData)
|
fun onLocationUpdate(locationData: LocationData)
|
||||||
fun onLocationProviderIsNotAvailable()
|
fun onLocationProviderIsNotAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var locationManager: LocationManager? = null
|
private var callback: Callback? = null
|
||||||
var callback: Callback? = null
|
|
||||||
|
private var hasGpsProviderLiveLocation = false
|
||||||
|
|
||||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
||||||
fun start() {
|
fun start(callback: Callback?) {
|
||||||
val locationManager = context.getSystemService<LocationManager>()
|
Timber.d("## LocationTracker. start()")
|
||||||
|
hasGpsProviderLiveLocation = false
|
||||||
|
this.callback = callback
|
||||||
|
|
||||||
locationManager?.let {
|
if (locationManager == null) {
|
||||||
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 {
|
|
||||||
callback?.onLocationProviderIsNotAvailable()
|
callback?.onLocationProviderIsNotAvailable()
|
||||||
Timber.v("## LocationTracker. LocationManager is not available")
|
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])
|
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
||||||
fun stop() {
|
fun stop() {
|
||||||
|
Timber.d("## LocationTracker. stop()")
|
||||||
locationManager?.removeUpdates(this)
|
locationManager?.removeUpdates(this)
|
||||||
callback = null
|
callback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLocationChanged(location: Location) {
|
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())
|
callback?.onLocationUpdate(location.toLocationData())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onProviderDisabled(provider: String) {
|
||||||
|
Timber.d("## LocationTracker. onProviderDisabled: $provider")
|
||||||
|
callback?.onLocationProviderIsNotAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
private fun Location.toLocationData(): LocationData {
|
private fun Location.toLocationData(): LocationData {
|
||||||
return LocationData(latitude, longitude, accuracy.toDouble())
|
return LocationData(latitude, longitude, accuracy.toDouble())
|
||||||
}
|
}
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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
|
import android.graphics.drawable.Drawable
|
||||||
|
|
||||||
interface VectorMapView {
|
data class MapState(
|
||||||
fun initialize(onMapReady: () -> Unit)
|
val zoomOnlyOnce: Boolean,
|
||||||
|
val pinLocationData: LocationData? = null,
|
||||||
fun addPinToMap(pinId: String, image: Drawable)
|
val pinId: String,
|
||||||
fun updatePinLocation(pinId: String, latitude: Double, longitude: Double)
|
val pinDrawable: Drawable? = null
|
||||||
fun deleteAllPins()
|
)
|
||||||
|
|
||||||
fun zoomToLocation(latitude: Double, longitude: Double, zoom: Double)
|
|
||||||
fun getCurrentZoom(): Double?
|
|
||||||
|
|
||||||
fun onClick(callback: () -> Unit)
|
|
||||||
}
|
|
@ -17,7 +17,6 @@
|
|||||||
package im.vector.app.features.location
|
package im.vector.app.features.location
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition
|
import com.mapbox.mapboxsdk.camera.CameraPosition
|
||||||
import com.mapbox.mapboxsdk.geometry.LatLng
|
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.SymbolManager
|
||||||
import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions
|
import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions
|
||||||
import com.mapbox.mapboxsdk.style.layers.Property
|
import com.mapbox.mapboxsdk.style.layers.Property
|
||||||
import im.vector.app.BuildConfig
|
import timber.log.Timber
|
||||||
|
|
||||||
class MapTilerMapView @JvmOverloads constructor(
|
class MapTilerMapView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = 0
|
defStyleAttr: Int = 0
|
||||||
) : MapView(context, attrs, defStyleAttr), VectorMapView {
|
) : MapView(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
private var map: MapboxMap? = null
|
private var pendingState: MapState? = null
|
||||||
private var symbolManager: SymbolManager? = null
|
|
||||||
private var style: Style? = 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 ->
|
getMapAsync { map ->
|
||||||
map.setStyle(styleUrl) { style ->
|
map.setStyle(url) { style ->
|
||||||
this.symbolManager = SymbolManager(this, map, style)
|
mapRefs = MapRefs(
|
||||||
this.map = map
|
map,
|
||||||
this.style = style
|
SymbolManager(this, map, style),
|
||||||
onMapReady()
|
style
|
||||||
|
)
|
||||||
|
pendingState?.let { render(it) }
|
||||||
|
pendingState = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addPinToMap(pinId: String, image: Drawable) {
|
fun render(state: MapState) {
|
||||||
style?.addImage(pinId, image)
|
val safeMapRefs = mapRefs ?: return Unit.also {
|
||||||
|
pendingState = state
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updatePinLocation(pinId: String, latitude: Double, longitude: Double) {
|
state.pinDrawable?.let { pinDrawable ->
|
||||||
symbolManager?.create(
|
if (!safeMapRefs.style.isFullyLoaded ||
|
||||||
|
safeMapRefs.style.getImage(state.pinId) == null) {
|
||||||
|
safeMapRefs.style.addImage(state.pinId, pinDrawable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.pinLocationData?.let { locationData ->
|
||||||
|
if (!initZoomDone || !state.zoomOnlyOnce) {
|
||||||
|
zoomToLocation(locationData.latitude, locationData.longitude)
|
||||||
|
initZoomDone = true
|
||||||
|
}
|
||||||
|
|
||||||
|
safeMapRefs.symbolManager.deleteAll()
|
||||||
|
safeMapRefs.symbolManager.create(
|
||||||
SymbolOptions()
|
SymbolOptions()
|
||||||
.withLatLng(LatLng(latitude, longitude))
|
.withLatLng(LatLng(locationData.latitude, locationData.longitude))
|
||||||
.withIconImage(pinId)
|
.withIconImage(state.pinId)
|
||||||
.withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
|
.withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteAllPins() {
|
|
||||||
symbolManager?.deleteAll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun zoomToLocation(latitude: Double, longitude: Double, zoom: Double) {
|
private fun zoomToLocation(latitude: Double, longitude: Double) {
|
||||||
map?.cameraPosition = CameraPosition.Builder()
|
Timber.d("## Location: zoomToLocation")
|
||||||
|
mapRefs?.map?.cameraPosition = CameraPosition.Builder()
|
||||||
.target(LatLng(latitude, longitude))
|
.target(LatLng(latitude, longitude))
|
||||||
.zoom(zoom)
|
.zoom(INITIAL_MAP_ZOOM_IN_PREVIEW)
|
||||||
.build()
|
.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}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -48,4 +48,13 @@
|
|||||||
android:textColor="?colorPrimary"
|
android:textColor="?colorPrimary"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/shareLocationGpsLoading"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/shareLocationContainer"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/shareLocationContainer"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/shareLocationContainer" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -103,18 +103,34 @@
|
|||||||
tools:text="1080 x 1024 - 43s - 12kB"
|
tools:text="1080 x 1024 - 43s - 12kB"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<im.vector.app.features.location.MapTilerMapView
|
|
||||||
android:id="@+id/bottom_sheet_message_preview_location"
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/mapViewContainer"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="200dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="6dp"
|
||||||
android:contentDescription="@string/attachment_type_location"
|
|
||||||
android:scaleType="centerCrop"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
app:layout_constraintEnd_toEndOf="@id/bottom_sheet_message_preview_timestamp"
|
app:layout_constraintEnd_toEndOf="@id/bottom_sheet_message_preview_timestamp"
|
||||||
app:layout_constraintStart_toStartOf="@id/bottom_sheet_message_preview_sender"
|
app:layout_constraintStart_toStartOf="@id/bottom_sheet_message_preview_sender"
|
||||||
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
|
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
|
||||||
app:mapbox_renderTextureMode="true"
|
tools:alpha="0.3"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/staticMapImageView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="200dp"
|
||||||
|
android:contentDescription="@string/a11y_static_map_image" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/staticMapPinImageView"
|
||||||
|
android:layout_width="51dp"
|
||||||
|
android:layout_height="55dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginBottom="28dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:src="@drawable/bg_map_user_pin" />
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
@ -6,30 +6,19 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:cardCornerRadius="8dp">
|
app:cardCornerRadius="8dp">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<ImageView
|
||||||
android:id="@+id/mapViewContainer"
|
android:id="@+id/staticMapImageView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<im.vector.app.features.location.MapTilerMapView
|
|
||||||
android:id="@+id/mapView"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="200dp"
|
android:layout_height="200dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:contentDescription="@string/a11y_static_map_image" />
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:mapbox_renderTextureMode="true" />
|
|
||||||
|
|
||||||
<FrameLayout
|
<ImageView
|
||||||
android:id="@+id/clickableMapArea"
|
android:id="@+id/staticMapPinImageView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="51dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="55dp"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/mapView"
|
android:layout_gravity="center"
|
||||||
app:layout_constraintEnd_toEndOf="@id/mapView"
|
android:layout_marginBottom="28dp"
|
||||||
app:layout_constraintStart_toStartOf="@id/mapView"
|
android:importantForAccessibility="no"
|
||||||
app:layout_constraintTop_toTopOf="@id/mapView" />
|
android:src="@drawable/bg_map_user_pin" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
@ -3711,6 +3711,7 @@
|
|||||||
<string name="location_activity_title_static_sharing">Share location</string>
|
<string name="location_activity_title_static_sharing">Share location</string>
|
||||||
<string name="location_activity_title_preview">Location</string>
|
<string name="location_activity_title_preview">Location</string>
|
||||||
<string name="a11y_location_share_icon">Share location</string>
|
<string name="a11y_location_share_icon">Share location</string>
|
||||||
|
<string name="a11y_static_map_image">Map</string>
|
||||||
<string name="location_share">Share location</string>
|
<string name="location_share">Share location</string>
|
||||||
<string name="template_location_not_available_dialog_title">${app_name} could not access your location</string>
|
<string name="template_location_not_available_dialog_title">${app_name} could not access your location</string>
|
||||||
<string name="template_location_not_available_dialog_content">${app_name} could not access your location. Please try again later.</string>
|
<string name="template_location_not_available_dialog_content">${app_name} could not access your location. Please try again later.</string>
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user