Send location event.

This commit is contained in:
Onuray Sahin 2021-12-17 15:09:02 +03:00
parent 5904a5955f
commit 6495bd9e5e
13 changed files with 145 additions and 30 deletions

View file

@ -18,29 +18,17 @@ package org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LocationInfo( data class LocationInfo(
/** /**
* The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted. * Required. Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location.
*/ */
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null, @Json(name = "uri") val geoUri: String? = null,
/** /**
* Metadata about the image referred to in thumbnail_url. * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind
* of content description for accessibility e.g. 'location attachment'.
*/ */
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, @Json(name = "description") val description: String? = null
/**
* Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted.
*/
@Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
) )
/**
* Get the url of the encrypted thumbnail or of the thumbnail
*/
fun LocationInfo.getThumbnailUrl(): String? {
return thumbnailFile?.url ?: thumbnailUrl
}

View file

@ -26,7 +26,7 @@ data class MessageLocationContent(
/** /**
* Required. Must be 'm.location'. * Required. Must be 'm.location'.
*/ */
@Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String, @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String = MessageType.MSGTYPE_LOCATION,
/** /**
* Required. A description of the location e.g. 'Big Ben, London, UK', or some kind * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind
@ -35,14 +35,14 @@ data class MessageLocationContent(
@Json(name = "body") override val body: String, @Json(name = "body") override val body: String,
/** /**
* Required. A geo URI representing this location. * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location.
*/ */
@Json(name = "geo_uri") val geoUri: String, @Json(name = "geo_uri") val geoUri: String,
/** /**
* * See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
*/ */
@Json(name = "info") val locationInfo: LocationInfo? = null, @Json(name = "org.matrix.msc3488.location") val locationInfo: LocationInfo? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null

View file

@ -122,6 +122,14 @@ interface SendService {
*/ */
fun resendMediaMessage(localEcho: TimelineEvent): Cancelable fun resendMediaMessage(localEcho: TimelineEvent): Cancelable
/**
* Send a location event to the room
* @param latitude required latitude of the location
* @param longitude required longitude of the location
* @param uncertainty Accuracy of the location in meters
*/
fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable
/** /**
* Remove this failed message from the timeline * Remove this failed message from the timeline
* @param localEcho the unsent local echo * @param localEcho the unsent local echo

View file

@ -115,6 +115,12 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) } .let { sendEvent(it) }
} }
override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable {
return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
override fun redactEvent(event: Event, reason: String?): Cancelable { override fun redactEvent(event: Event, reason: String?): Cancelable {
// TODO manage media/attachements? // TODO manage media/attachements?
val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)

View file

@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.room.model.message.AudioInfo
import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo
import org.matrix.android.sdk.api.session.room.model.message.FileInfo import org.matrix.android.sdk.api.session.room.model.message.FileInfo
import org.matrix.android.sdk.api.session.room.model.message.ImageInfo import org.matrix.android.sdk.api.session.room.model.message.ImageInfo
import org.matrix.android.sdk.api.session.room.model.message.LocationInfo
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
@ -39,6 +40,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollConte
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
@ -194,6 +196,22 @@ internal class LocalEchoEventFactory @Inject constructor(
unsignedData = UnsignedData(age = null, transactionId = localId)) unsignedData = UnsignedData(age = null, transactionId = localId))
} }
fun createLocationEvent(roomId: String,
latitude: Double,
longitude: Double,
uncertainty: Double?): Event {
val geoUri = buildGeoUri(latitude, longitude, uncertainty)
val content = MessageLocationContent(
geoUri = geoUri,
body = geoUri,
locationInfo = LocationInfo(
geoUri = geoUri,
description = geoUri
)
)
return createMessageEvent(roomId, content)
}
fun createReplaceTextOfReply(roomId: String, fun createReplaceTextOfReply(roomId: String,
eventReplaced: TimelineEvent, eventReplaced: TimelineEvent,
originalEvent: TimelineEvent, originalEvent: TimelineEvent,
@ -463,6 +481,23 @@ internal class LocalEchoEventFactory @Inject constructor(
} }
} }
/**
* Returns RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30'
* Uncertainty of the location is in meters and not required.
*/
private fun buildGeoUri(latitude: Double, longitude: Double, uncertainty: Double?): String {
return buildString {
append("geo:")
append(latitude)
append(",")
append(longitude)
uncertainty?.let {
append(";")
append(it)
}
}
}
/* /*
* { * {
"content": { "content": {

View file

@ -0,0 +1,24 @@
/*
* 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
data class LocationData(
val latitude: Double,
val longitude: Double,
val uncertainty: Double?
)

View file

@ -19,5 +19,6 @@ 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 OnShareLocation(val latitude: Double, val longitude: Double) : LocationSharingAction() data class OnLocationUpdate(val locationData: LocationData) : LocationSharingAction()
object OnShareLocation : LocationSharingAction()
} }

View file

@ -26,6 +26,7 @@ import androidx.core.content.ContextCompat
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.mapbox.mapboxsdk.Mapbox import com.mapbox.mapboxsdk.Mapbox
import com.mapbox.mapboxsdk.camera.CameraPosition import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.geometry.LatLng import com.mapbox.mapboxsdk.geometry.LatLng
@ -79,6 +80,17 @@ class LocationSharingFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initMapView(savedInstanceState) initMapView(savedInstanceState)
views.shareLocationContainer.debouncedClicks {
viewModel.handle(LocationSharingAction.OnShareLocation)
}
viewModel.observeViewEvents {
when (it) {
LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError()
LocationSharingViewEvents.Close -> activity?.finish()
}
}
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -126,10 +138,10 @@ class LocationSharingFragment @Inject constructor(
} }
} }
override fun onLocationUpdate(latitude: Double, longitude: Double) { 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 map?.cameraPosition?.zoom ?: INITIAL_ZOOM
val latLng = LatLng(latitude, longitude) val latLng = LatLng(locationData.latitude, locationData.longitude)
map?.cameraPosition = CameraPosition.Builder() map?.cameraPosition = CameraPosition.Builder()
.target(latLng) .target(latLng)
@ -144,10 +156,20 @@ class LocationSharingFragment @Inject constructor(
.withIconImage(USER_PIN_NAME) .withIconImage(USER_PIN_NAME)
.withIconAnchor(Property.ICON_ANCHOR_BOTTOM) .withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
) )
viewModel.handle(LocationSharingAction.OnLocationUpdate(locationData))
}
private fun handleLocationNotAvailableError() {
MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.location_not_available_dialog_title)
.setMessage(R.string.location_not_available_dialog_content)
.setPositiveButton(R.string.ok, null)
.show()
} }
companion object { companion object {
const val INITIAL_ZOOM = 12.0 const val INITIAL_ZOOM = 15.0
const val USER_PIN_NAME = "USER_PIN_NAME" const val USER_PIN_NAME = "USER_PIN_NAME"
} }
} }

View file

@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewEvents
sealed class LocationSharingViewEvents : VectorViewEvents { sealed class LocationSharingViewEvents : VectorViewEvents {
object Close : LocationSharingViewEvents() object Close : LocationSharingViewEvents()
object LocationNotAvailableError : LocationSharingViewEvents()
} }

View file

@ -23,11 +23,15 @@ import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import 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
) : VectorViewModel<LocationSharingViewState, LocationSharingAction, LocationSharingViewEvents>(initialState) { ) : VectorViewModel<LocationSharingViewState, LocationSharingAction, LocationSharingViewEvents>(initialState) {
private val room = session.getRoom(initialState.roomId)!!
@AssistedFactory @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<LocationSharingViewModel, LocationSharingViewState> { interface Factory : MavericksAssistedViewModelFactory<LocationSharingViewModel, LocationSharingViewState> {
override fun create(initialState: LocationSharingViewState): LocationSharingViewModel override fun create(initialState: LocationSharingViewState): LocationSharingViewModel
@ -37,5 +41,28 @@ class LocationSharingViewModel @AssistedInject constructor(
} }
override fun handle(action: LocationSharingAction) { override fun handle(action: LocationSharingAction) {
when (action) {
is LocationSharingAction.OnLocationUpdate -> handleLocationUpdate(action.locationData)
LocationSharingAction.OnShareLocation -> handleShareLocation()
}
}
private fun handleShareLocation() = withState { state ->
state.lastKnownLocation?.let { location ->
room.sendLocation(
latitude = location.latitude,
longitude = location.longitude,
uncertainty = location.uncertainty
)
_viewEvents.post(LocationSharingViewEvents.Close)
} ?: run {
_viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError)
}
}
private fun handleLocationUpdate(locationData: LocationData) {
setState {
copy(lastKnownLocation = locationData)
}
} }
} }

View file

@ -27,7 +27,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
) : MavericksState { ) : MavericksState {
constructor(locationSharingArgs: LocationSharingArgs) : this( constructor(locationSharingArgs: LocationSharingArgs) : this(

View file

@ -27,7 +27,7 @@ class LocationTracker @Inject constructor(
private val context: Context) : LocationListener { private val context: Context) : LocationListener {
interface Callback { interface Callback {
fun onLocationUpdate(latitude: Double, longitude: Double) fun onLocationUpdate(locationData: LocationData)
} }
private var locationManager: LocationManager? = null private var locationManager: LocationManager? = null
@ -51,7 +51,7 @@ class LocationTracker @Inject constructor(
// Send last known location without waiting location updates // Send last known location without waiting location updates
it.getLastKnownLocation(provider)?.let { lastKnownLocation -> it.getLastKnownLocation(provider)?.let { lastKnownLocation ->
callback?.onLocationUpdate(lastKnownLocation.latitude, lastKnownLocation.longitude) callback?.onLocationUpdate(LocationData(lastKnownLocation.latitude, lastKnownLocation.longitude, lastKnownLocation.accuracy.toDouble()))
} }
it.requestLocationUpdates( it.requestLocationUpdates(
@ -70,7 +70,7 @@ class LocationTracker @Inject constructor(
} }
override fun onLocationChanged(location: Location) { override fun onLocationChanged(location: Location) {
callback?.onLocationUpdate(location.latitude, location.longitude) callback?.onLocationUpdate(LocationData(location.latitude, location.longitude, location.accuracy.toDouble()))
} }
companion object { companion object {

View file

@ -3708,4 +3708,6 @@
<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="location_share">Share location</string> <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>
</resources> </resources>