Show location preview and allow to share with external apps.

This commit is contained in:
Onuray Sahin 2021-12-27 14:03:59 +03:00
parent 6495bd9e5e
commit a0afab45fb
26 changed files with 601 additions and 100 deletions

View file

@ -36,6 +36,7 @@ import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Mavericks
import com.facebook.stetho.Stetho
import com.gabrielittner.threetenbp.LazyThreeTen
import com.mapbox.mapboxsdk.Mapbox
import com.vanniktech.emoji.EmojiManager
import com.vanniktech.emoji.google.GoogleEmojiProvider
import dagger.hilt.android.HiltAndroidApp
@ -195,6 +196,9 @@ class VectorApplication :
})
EmojiManager.install(GoogleEmojiProvider())
// Initialize Mapbox before inflating mapViews
Mapbox.getInstance(this)
}
private val startSyncOnFirstStart = object : DefaultLifecycleObserver {

View file

@ -61,6 +61,7 @@ import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
import im.vector.app.features.home.room.detail.RoomDetailFragment
import im.vector.app.features.home.room.detail.search.SearchFragment
import im.vector.app.features.home.room.list.RoomListFragment
import im.vector.app.features.location.LocationPreviewFragment
import im.vector.app.features.location.LocationSharingFragment
import im.vector.app.features.login.LoginCaptchaFragment
import im.vector.app.features.login.LoginFragment
@ -861,4 +862,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(LocationSharingFragment::class)
fun bindLocationSharingFragment(fragment: LocationSharingFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LocationPreviewFragment::class)
fun bindLocationPreviewFragment(fragment: LocationPreviewFragment): Fragment
}

View file

@ -297,6 +297,26 @@ fun openMedia(activity: Activity, savedMediaPath: String, mimeType: String) {
}
}
/**
* Open external location
* @param activity the activity
* @param latitude latitude of the location
* @param longitude longitude of the location
*/
fun openLocation(activity: Activity, latitude: Double, longitude: Double) {
val locationUri = buildString {
append("geo:")
append(latitude)
append(",")
append(longitude)
append("?q=") // This is required to drop a pin to the location
append(latitude)
append(",")
append(longitude)
}
openUri(activity, locationUri)
}
fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
val mediaUri = try {
FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", file)

View file

@ -20,6 +20,7 @@ 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
@ -110,4 +111,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
// Poll
data class EndPoll(val eventId: String) : RoomDetailAction()
// Location
data class ShowLocation(val locationData: LocationData, val userId: String) : RoomDetailAction()
}

View file

@ -465,6 +465,7 @@ class RoomDetailFragment @Inject constructor(
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
is RoomDetailViewEvents.ShowLocation -> handleShowLocationPreview(it)
}.exhaustive
}
@ -596,6 +597,17 @@ class RoomDetailFragment @Inject constructor(
}
}
private fun handleShowLocationPreview(viewEvent: RoomDetailViewEvents.ShowLocation) {
navigator
.openLocationSharing(
context = requireContext(),
roomId = roomDetailArgs.roomId,
mode = LocationSharingMode.PREVIEW,
initialLocationData = viewEvent.locationData,
locationOwnerId = viewEvent.userId
)
}
private fun requestNativeWidgetPermission(it: RoomDetailViewEvents.RequestNativeWidgetPermission) {
val tag = RoomWidgetPermissionBottomSheet::class.java.name
val dFrag = childFragmentManager.findFragmentByTag(tag) as? RoomWidgetPermissionBottomSheet
@ -2221,7 +2233,14 @@ class RoomDetailFragment @Inject constructor(
AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment)
AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomDetailArgs.roomId)
AttachmentTypeSelectorView.Type.LOCATION -> {
navigator.openLocationSharing(requireContext(), roomDetailArgs.roomId, LocationSharingMode.STATIC_SHARING)
navigator
.openLocationSharing(
context = requireContext(),
roomId = roomDetailArgs.roomId,
mode = LocationSharingMode.STATIC_SHARING,
initialLocationData = null,
locationOwnerId = session.myUserId
)
}
}.exhaustive
}

View file

@ -20,6 +20,7 @@ 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
@ -81,4 +82,6 @@ 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()
}

View file

@ -50,6 +50,7 @@ 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
@ -330,9 +331,14 @@ 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

View file

@ -37,6 +37,7 @@ 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
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
@ -49,6 +50,8 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem
import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
@ -67,6 +70,7 @@ 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.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer
import me.gujun.android.span.span
@ -82,6 +86,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithF
import org.matrix.android.sdk.api.session.room.model.message.MessageEmoteContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
import org.matrix.android.sdk.api.session.room.model.message.MessageNoticeContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
@ -116,7 +121,8 @@ class MessageItemFactory @Inject constructor(
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
private val spanUtils: SpanUtils,
private val session: Session,
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker) {
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
private val locationPinProvider: LocationPinProvider) {
// TODO inject this properly?
private var roomId: String = ""
@ -168,16 +174,36 @@ class MessageItemFactory @Inject constructor(
}
}
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessagePollContent -> buildPollContent(messageContent, informationData, highlight, callback, attributes)
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, callback, attributes)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
}
}
private fun buildPollContent(pollContent: MessagePollContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): PollItem? {
private fun buildLocationItem(locationContent: MessageLocationContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageLocationItem? {
val geoUri = locationContent.locationInfo?.geoUri ?: locationContent.geoUri
val locationData = LocationData.create(geoUri)
return MessageLocationItem_()
.attributes(attributes)
.locationData(locationData)
.userId(informationData.senderId)
.locationPinProvider(locationPinProvider)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
}
private fun buildPollItem(pollContent: MessagePollContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): PollItem? {
val optionViewStates = mutableListOf<PollOptionViewState>()
val pollResponseSummary = informationData.pollResponseAggregatedSummary

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.timeline.helper
import android.content.Context
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import androidx.core.content.ContextCompat
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import im.vector.app.R
import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.AvatarRenderer
import org.billcarsonfr.jsonviewer.Utils
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LocationPinProvider @Inject constructor(
private val context: Context,
private val session: Session,
private val avatarRenderer: AvatarRenderer
) {
private val cache = mutableMapOf<String, Drawable>()
private val glideRequests by lazy {
GlideApp.with(context)
}
fun create(userId: String, callback: (Drawable) -> Unit) {
if (cache.contains(userId)) {
callback(cache[userId]!!)
return
}
session.getUser(userId)?.toMatrixItem()?.let {
val size = Utils.dpToPx(44, context)
avatarRenderer.render(glideRequests, it, object : CustomTarget<Drawable>(size, size) {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
val bgUserPin = ContextCompat.getDrawable(context, R.drawable.bg_map_user_pin)!!
val layerDrawable = LayerDrawable(arrayOf(bgUserPin, resource))
val horizontalInset = Utils.dpToPx(4, context)
val topInset = Utils.dpToPx(4, context)
val bottomInset = Utils.dpToPx(8, context)
layerDrawable.setLayerInset(1, horizontalInset, topInset, horizontalInset, bottomInset)
cache[userId] = layerDrawable
callback(layerDrawable)
}
override fun onLoadCleared(placeholder: Drawable?) {
// Is it possible? Put placeholder instead?
}
})
}
}
}

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.timeline.item
import androidx.constraintlayout.widget.ConstraintLayout
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.onClick
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.LocationPinProvider
import im.vector.app.features.location.LocationData
import im.vector.app.features.location.MapTilerMapView
import im.vector.app.features.location.VectorMapListener
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>() {
@EpoxyAttribute
var callback: TimelineEventController.Callback? = null
@EpoxyAttribute
var locationData: LocationData? = null
@EpoxyAttribute
var userId: String? = null
@EpoxyAttribute
var locationPinProvider: LocationPinProvider? = null
override fun bind(holder: Holder) {
super.bind(holder)
renderSendState(holder.mapViewContainer, null)
val location = locationData ?: return
val locationOwnerId = userId ?: return
holder.mapView.initialize(object : VectorMapListener {
override fun onMapReady() {
holder.mapView.zoomToLocation(location.latitude, location.longitude, INITIAL_ZOOM)
locationPinProvider?.create(locationOwnerId) { pinDrawable ->
holder.mapView.addPinToMap(locationOwnerId, pinDrawable)
holder.mapView.updatePinLocation(locationOwnerId, location.latitude, location.longitude)
}
holder.mapView.onClick {
callback?.onTimelineItemAction(RoomDetailAction.ShowLocation(location, locationOwnerId))
}
}
})
}
override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val mapViewContainer by bind<ConstraintLayout>(R.id.mapViewContainer)
val mapView by bind<MapTilerMapView>(R.id.mapView)
}
companion object {
private const val STUB_ID = R.id.messageContentLocationStub
private const val INITIAL_ZOOM = 15.0
}
}

View file

@ -16,9 +16,56 @@
package im.vector.app.features.location
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class LocationData(
val latitude: Double,
val longitude: Double,
val uncertainty: Double?
)
) : Parcelable {
fun toGeoUri(): String {
return buildString {
append("geo:")
append(latitude)
append(",")
append(longitude)
append("?q=")
append(latitude)
append(",")
append(longitude)
}
}
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)
return if (latitude != null && longitude != null) {
LocationData(
latitude = latitude.toDouble(),
longitude = longitude.toDouble(),
uncertainty = uncertainty?.toDouble()
)
} else null
}
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.location
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentLocationPreviewBinding
import javax.inject.Inject
import com.airbnb.mvrx.args
import im.vector.app.R
import im.vector.app.core.utils.openLocation
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import org.matrix.android.sdk.api.extensions.tryOrNull
class LocationPreviewFragment @Inject constructor(
private val locationPinProvider: LocationPinProvider
) : VectorBaseFragment<FragmentLocationPreviewBinding>(), VectorMapListener {
private val args: LocationSharingArgs by args()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationPreviewBinding {
return FragmentLocationPreviewBinding.inflate(layoutInflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.mapView.initialize(this)
}
override fun getMenuRes() = R.menu.menu_location_preview
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.share_external -> {
onShareLocationExternal()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun onShareLocationExternal() {
val location = args.initialLocationData ?: return
openLocation(requireActivity(), location.latitude, location.longitude)
}
override fun onMapReady() {
val location = args.initialLocationData ?: return
val userId = args.locationOwnerId
locationPinProvider.create(userId) { pinDrawable ->
views.mapView.apply {
zoomToLocation(location.latitude, location.longitude, INITIAL_ZOOM)
deleteAllPins()
addPinToMap(userId, pinDrawable)
updatePinLocation(userId, location.latitude, location.longitude)
}
}
}
companion object {
const val INITIAL_ZOOM = 15.0
}
}

View file

@ -30,7 +30,9 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class LocationSharingArgs(
val roomId: String,
val mode: LocationSharingMode
val mode: LocationSharingMode,
val initialLocationData: LocationData?,
val locationOwnerId: String
) : Parcelable
@AndroidEntryPoint
@ -62,6 +64,11 @@ class LocationSharingActivity : VectorBaseActivity<ActivityLocationSharingBindin
)
}
LocationSharingMode.PREVIEW -> {
addFragment(
views.fragmentContainer,
LocationPreviewFragment::class.java,
locationSharingArgs
)
}
}
}

View file

@ -16,41 +16,24 @@
package im.vector.app.features.location
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import com.airbnb.mvrx.activityViewModel
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.mapbox.mapboxsdk.Mapbox
import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.maps.MapboxMap
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 im.vector.app.R
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentLocationSharingBinding
import im.vector.app.features.home.AvatarRenderer
import org.billcarsonfr.jsonviewer.Utils
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.util.toMatrixItem
import javax.inject.Inject
class LocationSharingFragment @Inject constructor(
private val locationTracker: LocationTracker,
private val session: Session,
private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment<FragmentLocationSharingBinding>(), LocationTracker.Callback {
private val locationPinProvider: LocationPinProvider
) : VectorBaseFragment<FragmentLocationSharingBinding>(), LocationTracker.Callback, VectorMapListener {
init {
locationTracker.callback = this
@ -58,28 +41,16 @@ class LocationSharingFragment @Inject constructor(
private val viewModel: LocationSharingViewModel by activityViewModel()
private val glideRequests by lazy {
GlideApp.with(this)
}
private var map: MapboxMap? = null
private var symbolManager: SymbolManager? = null
private var lastZoomValue: Double = -1.0
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationSharingBinding {
return FragmentLocationSharingBinding.inflate(inflater, container, false)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize Mapbox before inflating mapView
Mapbox.getInstance(requireContext())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initMapView(savedInstanceState)
views.mapView.initialize(this)
views.shareLocationContainer.debouncedClicks {
viewModel.handle(LocationSharingAction.OnShareLocation)
@ -98,64 +69,23 @@ class LocationSharingFragment @Inject constructor(
locationTracker.stop()
}
private fun initMapView(savedInstanceState: Bundle?) {
val key = BuildConfig.mapTilerKey
val styleUrl = "https://api.maptiler.com/maps/streets/style.json?key=$key"
views.mapView.onCreate(savedInstanceState)
views.mapView.getMapAsync { map ->
map.setStyle(styleUrl) { style ->
addUserPinToMap(style)
this.symbolManager = SymbolManager(views.mapView, map, style)
this.map = map
// All set, start location tracker
locationTracker.start()
}
}
}
private fun addUserPinToMap(style: Style) {
session.getUser(session.myUserId)?.toMatrixItem()?.let {
val size = Utils.dpToPx(44, requireContext())
avatarRenderer.render(glideRequests, it, object : CustomTarget<Drawable>(size, size) {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
val bgUserPin = ContextCompat.getDrawable(requireActivity(), R.drawable.bg_map_user_pin)!!
val layerDrawable = LayerDrawable(arrayOf(bgUserPin, resource))
val horizontalInset = Utils.dpToPx(4, requireContext())
val topInset = Utils.dpToPx(4, requireContext())
val bottomInset = Utils.dpToPx(8, requireContext())
layerDrawable.setLayerInset(1, horizontalInset, topInset, horizontalInset, bottomInset)
style.addImage(
USER_PIN_NAME,
layerDrawable
)
}
override fun onLoadCleared(placeholder: Drawable?) {
// Is it possible? Put placeholder instead?
}
})
override fun onMapReady() {
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_ZOOM else map?.cameraPosition?.zoom ?: INITIAL_ZOOM
lastZoomValue = if (lastZoomValue == -1.0) INITIAL_ZOOM else views.mapView.getCurrentZoom() ?: INITIAL_ZOOM
val latLng = LatLng(locationData.latitude, locationData.longitude)
map?.cameraPosition = CameraPosition.Builder()
.target(latLng)
.zoom(lastZoomValue)
.build()
symbolManager?.deleteAll()
symbolManager?.create(
SymbolOptions()
.withLatLng(latLng)
.withIconImage(USER_PIN_NAME)
.withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
)
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))
}

View file

@ -67,6 +67,7 @@ class LocationTracker @Inject constructor(
fun stop() {
locationManager?.removeUpdates(this)
callback = null
}
override fun onLocationChanged(location: Location) {

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.location
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import com.mapbox.mapboxsdk.Mapbox
import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.maps.MapView
import com.mapbox.mapboxsdk.maps.MapboxMap
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
class MapTilerMapView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : MapView(context, attrs, defStyleAttr), VectorMapView {
private var map: MapboxMap? = null
private var symbolManager: SymbolManager? = null
private var style: Style? = null
override fun initialize(listener: VectorMapListener) {
getMapAsync { map ->
map.setStyle(styleUrl) { style ->
this.symbolManager = SymbolManager(this, map, style)
this.map = map
this.style = style
listener.onMapReady()
}
}
}
override fun addPinToMap(pinId: String, image: Drawable) {
style?.addImage(pinId, image)
}
override fun updatePinLocation(pinId: String, latitude: Double, longitude: Double) {
symbolManager?.create(
SymbolOptions()
.withLatLng(LatLng(latitude, longitude))
.withIconImage(pinId)
.withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
)
}
override fun deleteAllPins() {
symbolManager?.deleteAll()
}
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
}
}
companion object {
private const val styleUrl = "https://api.maptiler.com/maps/streets/style.json?key=${BuildConfig.mapTilerKey}"
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.location
import android.graphics.drawable.Drawable
interface VectorMapListener {
fun onMapReady()
}
interface VectorMapView {
fun initialize(listener: VectorMapListener)
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)
}

View file

@ -57,6 +57,7 @@ import im.vector.app.features.home.room.detail.search.SearchActivity
import im.vector.app.features.home.room.detail.search.SearchArgs
import im.vector.app.features.home.room.filtered.FilteredRoomsActivity
import im.vector.app.features.invite.InviteUsersToRoomActivity
import im.vector.app.features.location.LocationData
import im.vector.app.features.location.LocationSharingActivity
import im.vector.app.features.location.LocationSharingArgs
import im.vector.app.features.location.LocationSharingMode
@ -536,10 +537,14 @@ class DefaultNavigator @Inject constructor(
context.startActivity(intent)
}
override fun openLocationSharing(context: Context, roomId: String, mode: LocationSharingMode) {
override fun openLocationSharing(context: Context,
roomId: String,
mode: LocationSharingMode,
initialLocationData: LocationData?,
locationOwnerId: String) {
val intent = LocationSharingActivity.getIntent(
context,
LocationSharingArgs(roomId = roomId, mode = mode)
LocationSharingArgs(roomId = roomId, mode = mode, initialLocationData = initialLocationData, locationOwnerId = locationOwnerId)
)
context.startActivity(intent)
}

View file

@ -25,6 +25,7 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.core.util.Pair
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.location.LocationData
import im.vector.app.features.location.LocationSharingMode
import im.vector.app.features.login.LoginConfig
import im.vector.app.features.media.AttachmentData
@ -151,5 +152,9 @@ interface Navigator {
fun openCreatePoll(context: Context, roomId: String)
fun openLocationSharing(context: Context, roomId: String, mode: LocationSharingMode)
fun openLocationSharing(context: Context,
roomId: String,
mode: LocationSharingMode,
initialLocationData: LocationData?,
locationOwnerId: String)
}

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#0DBD8B" android:fillType="evenOdd" android:pathData="M21.768,2.8608V3.0305C21.7809,3.0862 21.7889,3.143 21.792,3.2002V8.4608C21.792,8.718 21.6908,8.9646 21.5108,9.1465C21.3308,9.3283 21.0866,9.4305 20.832,9.4305C20.5774,9.4305 20.3332,9.3283 20.1531,9.1465C19.9731,8.9646 19.872,8.718 19.872,8.4608V5.5517L11.256,14.2547C11.1661,14.3455 11.0595,14.4174 10.9421,14.4665C10.8248,14.5156 10.699,14.5409 10.572,14.5409C10.4449,14.5409 10.3192,14.5156 10.2018,14.4665C10.0844,14.4174 9.9778,14.3455 9.888,14.2547C9.7982,14.164 9.7269,14.0563 9.6783,13.9377C9.6297,13.8192 9.6046,13.6921 9.6046,13.5638C9.6046,13.4355 9.6297,13.3085 9.6783,13.1899C9.7269,13.0714 9.7982,12.9636 9.888,12.8729L18.504,4.2426H15.624C15.4979,4.2426 15.3731,4.2175 15.2566,4.1688C15.1401,4.1201 15.0343,4.0486 14.9451,3.9586C14.856,3.8686 14.7853,3.7617 14.737,3.644C14.6888,3.5264 14.664,3.4003 14.664,3.2729C14.664,3.1456 14.6888,3.0195 14.737,2.9018C14.7853,2.7842 14.856,2.6773 14.9451,2.5872C15.0343,2.4972 15.1401,2.4258 15.2566,2.377C15.3731,2.3283 15.4979,2.3032 15.624,2.3032H21.192L21.288,2.3517L21.36,2.4002L21.552,2.5457L21.672,2.6911L21.72,2.7638L21.768,2.8608ZM16.464,22.0122H5.088C4.3242,22.0122 3.5917,21.7057 3.0515,21.1602C2.5114,20.6146 2.208,19.8747 2.208,19.1031V7.6122C2.208,6.8407 2.5114,6.1007 3.0515,5.5552C3.5917,5.0096 4.3242,4.7031 5.088,4.7031H11.88C12.1346,4.7031 12.3788,4.8053 12.5588,4.9871C12.7389,5.169 12.84,5.4156 12.84,5.6728C12.84,5.93 12.7389,6.1767 12.5588,6.3585C12.3788,6.5403 12.1346,6.6425 11.88,6.6425H5.088C4.8334,6.6425 4.5892,6.7447 4.4092,6.9265C4.2291,7.1084 4.128,7.355 4.128,7.6122V19.1031C4.128,19.3603 4.2291,19.607 4.4092,19.7888C4.5892,19.9707 4.8334,20.0728 5.088,20.0728H16.464C16.7186,20.0728 16.9628,19.9707 17.1428,19.7888C17.3229,19.607 17.424,19.3603 17.424,19.1031V12.2425C17.424,11.9853 17.5252,11.7387 17.7052,11.5568C17.8852,11.375 18.1294,11.2728 18.384,11.2728C18.6386,11.2728 18.8828,11.375 19.0628,11.5568C19.2429,11.7387 19.344,11.9853 19.344,12.2425V19.1031C19.344,19.8747 19.0406,20.6146 18.5005,21.1602C17.9604,21.7057 17.2278,22.0122 16.464,22.0122Z"/>
</vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<im.vector.app.features.location.MapTilerMapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -4,7 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.mapbox.mapboxsdk.maps.MapView
<im.vector.app.features.location.MapTilerMapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View file

@ -130,6 +130,11 @@
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_poll" />
<ViewStub
android:id="@+id/messageContentLocationStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_location_stub" />
</FrameLayout>
<im.vector.app.core.ui.views.SendStateImageView

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/mapViewContainer"
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"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/share_external"
android:icon="@drawable/ic_share_external"
android:title="@string/location_share_external"
app:iconTint="?colorPrimary"
app:showAsAction="always" />
</menu>

View file

@ -3710,4 +3710,5 @@
<string name="location_share">Share location</string>
<string name="location_not_available_dialog_title">Element could not access your location</string>
<string name="location_not_available_dialog_content">Element could not access your location. Please try again later.</string>
<string name="location_share_external">Open with</string>
</resources>