mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 10:25:35 +03:00
Send location event.
This commit is contained in:
parent
5904a5955f
commit
6495bd9e5e
13 changed files with 145 additions and 30 deletions
|
@ -18,29 +18,17 @@ package org.matrix.android.sdk.api.session.room.model.message
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
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,
|
||||
|
||||
/**
|
||||
* 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
|
||||
@Json(name = "description") val description: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Get the url of the encrypted thumbnail or of the thumbnail
|
||||
*/
|
||||
fun LocationInfo.getThumbnailUrl(): String? {
|
||||
return thumbnailFile?.url ?: thumbnailUrl
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ data class MessageLocationContent(
|
|||
/**
|
||||
* 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
|
||||
|
@ -35,14 +35,14 @@ data class MessageLocationContent(
|
|||
@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,
|
||||
|
||||
/**
|
||||
*
|
||||
* 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.new_content") override val newContent: Content? = null
|
||||
|
|
|
@ -122,6 +122,14 @@ interface SendService {
|
|||
*/
|
||||
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
|
||||
* @param localEcho the unsent local echo
|
||||
|
|
|
@ -115,6 +115,12 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
.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 {
|
||||
// TODO manage media/attachements?
|
||||
val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
|
||||
|
|
|
@ -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.FileInfo
|
||||
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.MessageContent
|
||||
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.MessageFormat
|
||||
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.MessagePollResponseContent
|
||||
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))
|
||||
}
|
||||
|
||||
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,
|
||||
eventReplaced: 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": {
|
||||
|
|
|
@ -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?
|
||||
)
|
||||
|
|
@ -19,5 +19,6 @@ package im.vector.app.features.location
|
|||
import im.vector.app.core.platform.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()
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ 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
|
||||
|
@ -79,6 +80,17 @@ class LocationSharingFragment @Inject constructor(
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
initMapView(savedInstanceState)
|
||||
|
||||
views.shareLocationContainer.debouncedClicks {
|
||||
viewModel.handle(LocationSharingAction.OnShareLocation)
|
||||
}
|
||||
|
||||
viewModel.observeViewEvents {
|
||||
when (it) {
|
||||
LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError()
|
||||
LocationSharingViewEvents.Close -> activity?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
val latLng = LatLng(latitude, longitude)
|
||||
val latLng = LatLng(locationData.latitude, locationData.longitude)
|
||||
|
||||
map?.cameraPosition = CameraPosition.Builder()
|
||||
.target(latLng)
|
||||
|
@ -144,10 +156,20 @@ class LocationSharingFragment @Inject constructor(
|
|||
.withIconImage(USER_PIN_NAME)
|
||||
.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 {
|
||||
const val INITIAL_ZOOM = 12.0
|
||||
const val INITIAL_ZOOM = 15.0
|
||||
const val USER_PIN_NAME = "USER_PIN_NAME"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewEvents
|
|||
|
||||
sealed class LocationSharingViewEvents : VectorViewEvents {
|
||||
object Close : LocationSharingViewEvents()
|
||||
object LocationNotAvailableError : LocationSharingViewEvents()
|
||||
}
|
||||
|
|
|
@ -23,11 +23,15 @@ import dagger.assisted.AssistedInject
|
|||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
||||
class LocationSharingViewModel @AssistedInject constructor(
|
||||
@Assisted private val initialState: LocationSharingViewState
|
||||
@Assisted private val initialState: LocationSharingViewState,
|
||||
session: Session
|
||||
) : VectorViewModel<LocationSharingViewState, LocationSharingAction, LocationSharingViewEvents>(initialState) {
|
||||
|
||||
private val room = session.getRoom(initialState.roomId)!!
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<LocationSharingViewModel, LocationSharingViewState> {
|
||||
override fun create(initialState: LocationSharingViewState): LocationSharingViewModel
|
||||
|
@ -37,5 +41,28 @@ class LocationSharingViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,8 @@ enum class LocationSharingMode(@StringRes val titleRes: Int) {
|
|||
|
||||
data class LocationSharingViewState(
|
||||
val roomId: String,
|
||||
val mode: LocationSharingMode
|
||||
val mode: LocationSharingMode,
|
||||
val lastKnownLocation: LocationData? = null
|
||||
) : MavericksState {
|
||||
|
||||
constructor(locationSharingArgs: LocationSharingArgs) : this(
|
||||
|
|
|
@ -27,7 +27,7 @@ class LocationTracker @Inject constructor(
|
|||
private val context: Context) : LocationListener {
|
||||
|
||||
interface Callback {
|
||||
fun onLocationUpdate(latitude: Double, longitude: Double)
|
||||
fun onLocationUpdate(locationData: LocationData)
|
||||
}
|
||||
|
||||
private var locationManager: LocationManager? = null
|
||||
|
@ -51,7 +51,7 @@ class LocationTracker @Inject constructor(
|
|||
|
||||
// Send last known location without waiting location updates
|
||||
it.getLastKnownLocation(provider)?.let { lastKnownLocation ->
|
||||
callback?.onLocationUpdate(lastKnownLocation.latitude, lastKnownLocation.longitude)
|
||||
callback?.onLocationUpdate(LocationData(lastKnownLocation.latitude, lastKnownLocation.longitude, lastKnownLocation.accuracy.toDouble()))
|
||||
}
|
||||
|
||||
it.requestLocationUpdates(
|
||||
|
@ -70,7 +70,7 @@ class LocationTracker @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onLocationChanged(location: Location) {
|
||||
callback?.onLocationUpdate(location.latitude, location.longitude)
|
||||
callback?.onLocationUpdate(LocationData(location.latitude, location.longitude, location.accuracy.toDouble()))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -3708,4 +3708,6 @@
|
|||
<string name="location_activity_title_preview">Location</string>
|
||||
<string name="a11y_location_share_icon">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>
|
||||
|
|
Loading…
Reference in a new issue