diff --git a/changelog.d/2210.bugfix b/changelog.d/2210.bugfix
new file mode 100644
index 0000000000..6f7c09ce26
--- /dev/null
+++ b/changelog.d/2210.bugfix
@@ -0,0 +1 @@
+Static location sharing and rendering
\ No newline at end of file
diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle
index fd36f5110c..7de8100469 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -83,6 +83,7 @@ ext.groups = [
'com.jakewharton.android.repackaged',
'com.jakewharton.timber',
'com.linkedin.dexmaker',
+ 'com.mapbox.mapboxsdk',
'com.nulab-inc',
'com.otaliastudios.opengl',
'com.parse.bolts',
@@ -159,6 +160,7 @@ ext.groups = [
'org.junit.jupiter',
'org.junit.platform',
'org.jvnet.staxex',
+ 'org.maplibre.gl',
'org.matrix.android',
'org.mockito',
'org.mongodb',
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
new file mode 100644
index 0000000000..e8b3cf2488
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.room.model.message
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class LocationAsset(
+ @Json(name = "type") val type: LocationAssetType? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
new file mode 100644
index 0000000000..ef40e21c47
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.session.room.model.message
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class LocationAssetType {
+ @Json(name = "m.self")
+ SELF
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt
index a76c3c5b64..a1fd3bd2ec 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt
@@ -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. 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
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
index 6881c09924..2f3db8ff51 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
@@ -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,15 +35,32 @@ 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
-) : MessageContent
+ @Json(name = "m.new_content") override val newContent: Content? = null,
+
+ /**
+ * m.asset defines a generic asset that can be used for location tracking but also in other places like inventories, geofencing, checkins/checkouts etc.
+ * It should contain a mandatory namespaced type key defining what particular asset is being referred to.
+ * For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid.
+ */
+ @Json(name = "m.asset") val locationAsset: LocationAsset? = null,
+
+ /**
+ * Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
+ */
+ @Json(name = "org.matrix.msc3488.ts") val ts: Long? = null,
+
+ @Json(name = "org.matrix.msc1767.text") val text: String? = null
+) : MessageContent {
+
+ fun getUri() = locationInfo?.geoUri ?: geoUri
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
index 5e1b430207..20d00394df 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
@@ -133,6 +133,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
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index 9d105120e2..5662a72cb8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -122,6 +122,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)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
index 72ae688c4b..1e46602411 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
@@ -32,6 +32,9 @@ 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.LocationAsset
+import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType
+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 +42,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
@@ -63,6 +67,7 @@ import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor
import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
import java.util.UUID
+import java.util.concurrent.TimeUnit
import javax.inject.Inject
/**
@@ -224,6 +229,27 @@ 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
+ ),
+ locationAsset = LocationAsset(
+ type = LocationAssetType.SELF
+ ),
+ ts = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
+ text = geoUri
+ )
+ return createMessageEvent(roomId, content)
+ }
+
fun createReplaceTextOfReply(roomId: String,
eventReplaced: TimelineEvent,
originalEvent: TimelineEvent,
@@ -510,6 +536,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": {
diff --git a/vector/build.gradle b/vector/build.gradle
index 9203a8ec2f..0151950c84 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -153,6 +153,9 @@ android {
// This *must* only be set in trusted environments.
buildConfigField "Boolean", "handleCallAssertedIdentityEvents", "false"
+ buildConfigField "Boolean", "enableLocationSharing", "true"
+ buildConfigField "String", "mapTilerKey", "\"fU3vlMsMn4Jb6dnEIFsx\""
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// Keep abiFilter for the universalApk
@@ -498,6 +501,10 @@ dependencies {
}
implementation 'commons-codec:commons-codec:1.15'
+ // MapTiler
+ implementation 'org.maplibre.gl:android-sdk:9.5.2'
+ implementation 'org.maplibre.gl:android-plugin-annotation-v9:1.0.0'
+
// TESTS
testImplementation libs.tests.junit
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index b3f845d8c5..fdec5337ba 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -42,6 +42,10 @@
android:name="android.permission.WRITE_CALENDAR"
tools:node="remove" />
+
+
+
+
@@ -333,6 +337,7 @@
+
diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html
index 0eefa3b863..2c25606f57 100755
--- a/vector/src/main/assets/open_source_licenses.html
+++ b/vector/src/main/assets/open_source_licenses.html
@@ -254,6 +254,33 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+
+ -
+ org.maplibre.gl:android-sdk
+
+ org.maplibre.gl:android-plugin-annotation-v9
+
+ BSD 2-Clause License
+
+ Copyright (c) 2021 MapLibre contributors
+
+ Copyright (c) 2018-2021 MapTiler.com
+
+ Copyright (c) 2014-2020 Mapbox
+
+
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
-
textdrawable
diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt
index 05d20662c7..d252b5d9bd 100644
--- a/vector/src/main/java/im/vector/app/VectorApplication.kt
+++ b/vector/src/main/java/im/vector/app/VectorApplication.kt
@@ -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
@@ -197,6 +198,9 @@ class VectorApplication :
})
EmojiManager.install(GoogleEmojiProvider())
+
+ // Initialize Mapbox before inflating mapViews
+ Mapbox.getInstance(this)
}
private val startSyncOnFirstStart = object : DefaultLifecycleObserver {
diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
index 626cf90592..5d27909b25 100644
--- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
@@ -61,6 +61,8 @@ 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
import im.vector.app.features.login.LoginGenericTextInputFormFragment
@@ -939,4 +941,14 @@ interface FragmentModule {
@IntoMap
@FragmentKey(CreatePollFragment::class)
fun bindCreatePollFragment(fragment: CreatePollFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(LocationSharingFragment::class)
+ fun bindLocationSharingFragment(fragment: LocationSharingFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(LocationPreviewFragment::class)
+ fun bindLocationPreviewFragment(fragment: LocationPreviewFragment): Fragment
}
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index cc31a7dca6..9ad01cd3e4 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -54,6 +54,7 @@ import im.vector.app.features.home.room.detail.upgrade.MigrateRoomViewModel
import im.vector.app.features.home.room.list.RoomListViewModel
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
import im.vector.app.features.invite.InviteUsersToRoomViewModel
+import im.vector.app.features.location.LocationSharingViewModel
import im.vector.app.features.login.LoginViewModel
import im.vector.app.features.login2.LoginViewModel2
import im.vector.app.features.login2.created.AccountCreatedViewModel
@@ -588,4 +589,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(CreatePollViewModel::class)
fun createPollViewModelFactory(factory: CreatePollViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
+ @Binds
+ @IntoMap
+ @MavericksViewModelKey(LocationSharingViewModel::class)
+ fun createLocationSharingViewModelFactory(factory: LocationSharingViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}
diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt
index 8e61a1c1cb..cdecd2d6c6 100644
--- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt
+++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt
@@ -30,8 +30,11 @@ import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import im.vector.app.features.home.room.detail.timeline.item.BindingOptions
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
+import im.vector.app.features.location.LocationData
+import im.vector.app.features.location.MapTilerMapView
import im.vector.app.features.media.ImageContentRenderer
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
import org.matrix.android.sdk.api.util.MatrixItem
@@ -66,6 +69,12 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel
+ holder.mapView.initialize {
+ if (holder.view.isAttachedToWindow) {
+ holder.mapView.zoomToLocation(location.latitude, location.longitude, 15.0)
+ locationPinProvider?.create(matrixItem.id) { pinDrawable ->
+ holder.mapView.addPinToMap(matrixItem.id, pinDrawable)
+ holder.mapView.updatePinLocation(matrixItem.id, location.latitude, location.longitude)
+ }
+ }
+ }
+ }
}
override fun unbind(holder: Holder) {
@@ -101,5 +124,6 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel(R.id.bottom_sheet_message_preview_body_details)
val timestamp by bind(R.id.bottom_sheet_message_preview_timestamp)
val imagePreview by bind(R.id.bottom_sheet_message_preview_image)
+ val mapView by bind(R.id.bottom_sheet_message_preview_location)
}
}
diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt
index 478f4d882b..a9375b6545 100644
--- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt
+++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt
@@ -183,6 +183,26 @@ fun openMedia(activity: Activity, savedMediaPath: String, mimeType: String) {
activity.safeStartActivity(intent)
}
+/**
+ * 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)
diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt
index 19dc341f12..dabf11b9d3 100644
--- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt
+++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt
@@ -40,6 +40,7 @@ val PERMISSIONS_FOR_MEMBERS_SEARCH = listOf(Manifest.permission.READ_CONTACTS)
val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA)
val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS)
+val PERMISSIONS_FOR_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
val PERMISSIONS_EMPTY = emptyList()
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt
index 683c5908ba..a15bd52174 100644
--- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt
@@ -37,6 +37,7 @@ import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.utils.PERMISSIONS_EMPTY
+import im.vector.app.core.utils.PERMISSIONS_FOR_LOCATION_SHARING
import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding
@@ -71,6 +72,7 @@ class AttachmentTypeSelectorView(context: Context,
views.attachmentStickersButton.configure(Type.STICKER)
views.attachmentContactButton.configure(Type.CONTACT)
views.attachmentPollButton.configure(Type.POLL)
+ views.attachmentLocationButton.configure(Type.LOCATION)
width = LinearLayout.LayoutParams.MATCH_PARENT
height = LinearLayout.LayoutParams.WRAP_CONTENT
animationStyle = 0
@@ -123,12 +125,13 @@ class AttachmentTypeSelectorView(context: Context,
fun setAttachmentVisibility(type: Type, isVisible: Boolean) {
when (type) {
- Type.CAMERA -> views.attachmentCameraButton
- Type.GALLERY -> views.attachmentGalleryButton
- Type.FILE -> views.attachmentFileButton
- Type.STICKER -> views.attachmentStickersButton
- Type.CONTACT -> views.attachmentContactButton
- Type.POLL -> views.attachmentPollButton
+ Type.CAMERA -> views.attachmentCameraButton
+ Type.GALLERY -> views.attachmentGalleryButton
+ Type.FILE -> views.attachmentFileButton
+ Type.STICKER -> views.attachmentStickersButton
+ Type.CONTACT -> views.attachmentContactButton
+ Type.POLL -> views.attachmentPollButton
+ Type.LOCATION -> views.attachmentLocationButton
}.let {
it.isVisible = isVisible
}
@@ -211,6 +214,7 @@ class AttachmentTypeSelectorView(context: Context,
FILE(PERMISSIONS_EMPTY, R.string.tooltip_attachment_file),
STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker),
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact),
- POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll)
+ POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll),
+ LOCATION(PERMISSIONS_FOR_LOCATION_SHARING, R.string.tooltip_attachment_location)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
index f866bb328d..58e36d2303 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
@@ -20,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
@@ -111,4 +112,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
// Poll
data class EndPoll(val eventId: String) : RoomDetailAction()
+
+ // Location
+ data class ShowLocation(val locationData: LocationData, val userId: String) : RoomDetailAction()
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
index 72dbf1436f..9926ecad24 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
@@ -106,6 +106,7 @@ import im.vector.app.core.utils.createUIHandler
import im.vector.app.core.utils.isValidUrl
import im.vector.app.core.utils.onPermissionDeniedDialog
import im.vector.app.core.utils.onPermissionDeniedSnackbar
+import im.vector.app.core.utils.openLocation
import im.vector.app.core.utils.openUrlInExternalBrowser
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.safeStartActivity
@@ -170,6 +171,8 @@ import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.PillImageSpan
import im.vector.app.features.html.PillsPostProcessor
import im.vector.app.features.invite.VectorInviteView
+import im.vector.app.features.location.LocationData
+import im.vector.app.features.location.LocationSharingMode
import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer
import im.vector.app.features.notifications.NotificationDrawerManager
@@ -212,6 +215,7 @@ 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.MessageFormat
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.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
@@ -477,6 +481,7 @@ class RoomDetailFragment @Inject constructor(
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
+ is RoomDetailViewEvents.ShowLocation -> handleShowLocationPreview(it)
}.exhaustive
}
@@ -608,6 +613,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
@@ -1373,6 +1389,7 @@ class RoomDetailFragment @Inject constructor(
override fun onAddAttachment() {
if (!::attachmentTypeSelector.isInitialized) {
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomDetailFragment)
+ attachmentTypeSelector.setAttachmentVisibility(AttachmentTypeSelectorView.Type.LOCATION, vectorPreferences.isLocationSharingEnabled())
}
attachmentTypeSelector.show(views.composerLayout.views.attachmentButton)
}
@@ -1920,16 +1937,22 @@ class RoomDetailFragment @Inject constructor(
}
private fun onShareActionClicked(action: EventSharedAction.Share) {
- if (action.messageContent is MessageTextContent) {
- shareText(requireContext(), action.messageContent.body)
- } else if (action.messageContent is MessageWithAttachmentContent) {
- lifecycleScope.launch {
- val result = runCatching { session.fileService().downloadFile(messageContent = action.messageContent) }
- if (!isAdded) return@launch
- result.fold(
- { shareMedia(requireContext(), it, getMimeTypeFromUri(requireContext(), it.toUri())) },
- { showErrorInSnackbar(it) }
- )
+ when (action.messageContent) {
+ is MessageTextContent -> shareText(requireContext(), action.messageContent.body)
+ is MessageLocationContent -> {
+ LocationData.create(action.messageContent.getUri())?.let {
+ openLocation(requireActivity(), it.latitude, it.longitude)
+ }
+ }
+ is MessageWithAttachmentContent -> {
+ lifecycleScope.launch {
+ val result = runCatching { session.fileService().downloadFile(messageContent = action.messageContent) }
+ if (!isAdded) return@launch
+ result.fold(
+ { shareMedia(requireContext(), it, getMimeTypeFromUri(requireContext(), it.toUri())) },
+ { showErrorInSnackbar(it) }
+ )
+ }
}
}
}
@@ -2225,17 +2248,27 @@ class RoomDetailFragment @Inject constructor(
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
when (type) {
- AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(
+ AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(
activity = requireActivity(),
vectorPreferences = vectorPreferences,
cameraActivityResultLauncher = attachmentCameraActivityResultLauncher,
cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher
)
- AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
- AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
- AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
- AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment)
- AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomDetailArgs.roomId, null, PollMode.CREATE)
+ AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
+ AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
+ AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
+ AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment)
+ AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomDetailArgs.roomId, null, PollMode.CREATE)
+ AttachmentTypeSelectorView.Type.LOCATION -> {
+ navigator
+ .openLocationSharing(
+ context = requireContext(),
+ roomId = roomDetailArgs.roomId,
+ mode = LocationSharingMode.STATIC_SHARING,
+ initialLocationData = null,
+ locationOwnerId = session.myUserId
+ )
+ }
}.exhaustive
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt
index 86240a5ffe..b0921e01f9 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt
@@ -20,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
@@ -82,4 +83,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()
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
index 9149ae1dca..6e14b0fc76 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
@@ -53,6 +53,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
@@ -384,9 +385,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
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
index 27819ca863..1ff9679479 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
@@ -33,15 +33,19 @@ import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.format.EventDetailsFormatter
+import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.app.features.home.room.detail.timeline.tools.linkify
import im.vector.app.features.html.SpanUtils
+import im.vector.app.features.location.LocationData
import im.vector.app.features.media.ImageContentRenderer
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
import org.matrix.android.sdk.api.session.room.send.SendState
import javax.inject.Inject
@@ -57,7 +61,8 @@ class MessageActionsEpoxyController @Inject constructor(
private val errorFormatter: ErrorFormatter,
private val spanUtils: SpanUtils,
private val eventDetailsFormatter: EventDetailsFormatter,
- private val dateFormatter: VectorDateFormatter
+ private val dateFormatter: VectorDateFormatter,
+ private val locationPinProvider: LocationPinProvider
) : TypedEpoxyController() {
var listener: MessageActionsEpoxyControllerListener? = null
@@ -69,6 +74,9 @@ class MessageActionsEpoxyController @Inject constructor(
val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL)
val body = state.messageBody.linkify(host.listener)
val bindingOptions = spanUtils.getBindingOptions(body)
+ val locationData = state.timelineEvent()?.root?.getClearContent()?.toModel(catchError = true)?.let {
+ LocationData.create(it.getUri())
+ }
bottomSheetMessagePreviewItem {
id("preview")
avatarRenderer(host.avatarRenderer)
@@ -81,6 +89,8 @@ class MessageActionsEpoxyController @Inject constructor(
body(body.toEpoxyCharSequence())
bodyDetails(host.eventDetailsFormatter.format(state.timelineEvent()?.root)?.toEpoxyCharSequence())
time(formattedDate)
+ locationData(locationData)
+ locationPinProvider(host.locationPinProvider)
}
// Send state
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index 8e69e2d932..ea54d91a78 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -424,8 +424,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE,
- MessageType.MSGTYPE_POLL_START -> true
- else -> false
+ MessageType.MSGTYPE_POLL_START,
+ MessageType.MSGTYPE_LOCATION -> true
+ else -> false
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index d6bfc4ece7..eab7621d14 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -33,10 +33,12 @@ import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.core.utils.containsOnlyEmojis
+import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
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 +51,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,8 +71,10 @@ 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 im.vector.app.features.settings.VectorPreferences
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import me.gujun.android.span.span
import org.commonmark.node.Document
@@ -83,6 +89,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
@@ -118,7 +125,9 @@ 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,
+ private val vectorPreferences: VectorPreferences) {
// TODO inject this properly?
private var roomId: String = ""
@@ -170,16 +179,49 @@ 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 -> {
+ if (vectorPreferences.labsRenderLocationsInTimeline()) {
+ buildLocationItem(messageContent, informationData, highlight, callback, attributes)
+ } else {
+ buildMessageTextItem(messageContent.body, false, 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.getUri()
+ val locationData = LocationData.create(geoUri)
+
+ val mapCallback: MessageLocationItem.Callback = object : MessageLocationItem.Callback {
+ override fun onMapClicked() {
+ locationData?.let {
+ callback?.onTimelineItemAction(RoomDetailAction.ShowLocation(it, informationData.senderId))
+ }
+ }
+ }
+
+ return MessageLocationItem_()
+ .attributes(attributes)
+ .locationData(locationData)
+ .userId(informationData.senderId)
+ .locationPinProvider(locationPinProvider)
+ .highlighted(highlight)
+ .leftGuideline(avatarSizeProvider.leftGuideline)
+ .callback(mapCallback)
+ }
+
+ private fun buildPollItem(pollContent: MessagePollContent,
+ informationData: MessageInformationData,
+ highlight: Boolean,
+ callback: TimelineEventController.Callback?,
+ attributes: AbsMessageItem.Attributes): PollItem? {
val optionViewStates = mutableListOf()
val pollResponseSummary = informationData.pollResponseAggregatedSummary
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
index 3892bfff85..d5f3a74e4e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
@@ -89,6 +89,9 @@ class DisplayableEventFormatter @Inject constructor(
MessageType.MSGTYPE_FILE -> {
simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor)
}
+ MessageType.MSGTYPE_LOCATION -> {
+ simpleFormat(senderName, stringProvider.getString(R.string.sent_location), appendAuthor)
+ }
else -> {
simpleFormat(senderName, messageContent.body, appendAuthor)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt
new file mode 100644
index 0000000000..fe3a7d9007
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.di.ActiveSessionHolder
+import im.vector.app.core.glide.GlideApp
+import im.vector.app.core.utils.DimensionConverter
+import im.vector.app.features.home.AvatarRenderer
+import org.matrix.android.sdk.api.util.toMatrixItem
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class LocationPinProvider @Inject constructor(
+ private val context: Context,
+ private val activeSessionHolder: ActiveSessionHolder,
+ private val dimensionConverter: DimensionConverter,
+ private val avatarRenderer: AvatarRenderer
+) {
+ private val cache = mutableMapOf()
+
+ private val glideRequests by lazy {
+ GlideApp.with(context)
+ }
+
+ fun create(userId: String, callback: (Drawable) -> Unit) {
+ if (cache.contains(userId)) {
+ callback(cache[userId]!!)
+ return
+ }
+
+ activeSessionHolder.getActiveSession().getUser(userId)?.toMatrixItem()?.let {
+ val size = dimensionConverter.dpToPx(44)
+ avatarRenderer.render(glideRequests, it, object : CustomTarget(size, size) {
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ val bgUserPin = ContextCompat.getDrawable(context, R.drawable.bg_map_user_pin)!!
+ val layerDrawable = LayerDrawable(arrayOf(bgUserPin, resource))
+ val horizontalInset = dimensionConverter.dpToPx(4)
+ val topInset = dimensionConverter.dpToPx(4)
+ val bottomInset = dimensionConverter.dpToPx(8)
+ layerDrawable.setLayerInset(1, horizontalInset, topInset, horizontalInset, bottomInset)
+
+ cache[userId] = layerDrawable
+
+ callback(layerDrawable)
+ }
+
+ override fun onLoadCleared(placeholder: Drawable?) {
+ // Is it possible? Put placeholder instead?
+ }
+ })
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt
new file mode 100644
index 0000000000..3f030866a5
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt
@@ -0,0 +1,83 @@
+/*
+ * 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 android.widget.FrameLayout
+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.timeline.helper.LocationPinProvider
+import im.vector.app.features.location.LocationData
+import im.vector.app.features.location.MapTilerMapView
+
+@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
+abstract class MessageLocationItem : AbsMessageItem() {
+
+ interface Callback {
+ fun onMapClicked()
+ }
+
+ @EpoxyAttribute
+ var callback: Callback? = null
+
+ @EpoxyAttribute
+ var locationData: LocationData? = null
+
+ @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.clickableMapArea.onClick {
+ callback?.onMapClicked()
+ }
+
+ holder.mapView.apply {
+ initialize {
+ zoomToLocation(location.latitude, location.longitude, INITIAL_ZOOM)
+
+ locationPinProvider?.create(locationOwnerId) { pinDrawable ->
+ addPinToMap(locationOwnerId, pinDrawable)
+ updatePinLocation(locationOwnerId, location.latitude, location.longitude)
+ }
+ }
+ }
+ }
+
+ override fun getViewType() = STUB_ID
+
+ class Holder : AbsMessageItem.Holder(STUB_ID) {
+ val mapViewContainer by bind(R.id.mapViewContainer)
+ val mapView by bind(R.id.mapView)
+ val clickableMapArea by bind(R.id.clickableMapArea)
+ }
+
+ companion object {
+ private const val STUB_ID = R.id.messageContentLocationStub
+ private const val INITIAL_ZOOM = 15.0
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/Config.kt b/vector/src/main/java/im/vector/app/features/location/Config.kt
new file mode 100644
index 0000000000..630df16a37
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/Config.kt
@@ -0,0 +1,21 @@
+/*
+ * 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
+
+const val INITIAL_MAP_ZOOM = 15.0
+const val MIN_TIME_MILLIS_TO_UPDATE_LOCATION = 1 * 60 * 1000L // every 1 minute
+const val MIN_DISTANCE_METERS_TO_UPDATE_LOCATION = 10f
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationData.kt b/vector/src/main/java/im/vector/app/features/location/LocationData.kt
new file mode 100644
index 0000000000..c3ff09ebcd
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/LocationData.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class LocationData(
+ val latitude: Double,
+ val longitude: Double,
+ val uncertainty: Double?
+) : Parcelable {
+
+ companion object {
+
+ /**
+ * Creates location data from geo uri
+ * @param geoUri geo:latitude,longitude;uncertainty
+ * @return location data or null if geo uri is not valid
+ */
+ fun create(geoUri: String): LocationData? {
+ val geoParts = geoUri
+ .split(":")
+ .takeIf { it.firstOrNull() == "geo" }
+ ?.getOrNull(1)
+ ?.split(",")
+
+ val latitude = geoParts?.firstOrNull()
+ val geoTailParts = geoParts?.getOrNull(1)?.split(";")
+ val longitude = geoTailParts?.firstOrNull()
+ val uncertainty = geoTailParts?.getOrNull(1)?.replace("u=", "")
+
+ return if (latitude != null && longitude != null) {
+ LocationData(
+ latitude = latitude.toDouble(),
+ longitude = longitude.toDouble(),
+ uncertainty = uncertainty?.toDouble()
+ )
+ } else null
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt
new file mode 100644
index 0000000000..6209bf5a4f
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.os.Bundle
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import com.airbnb.mvrx.args
+import im.vector.app.R
+import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.core.utils.openLocation
+import im.vector.app.databinding.FragmentLocationPreviewBinding
+import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
+import javax.inject.Inject
+
+class LocationPreviewFragment @Inject constructor(
+ private val locationPinProvider: LocationPinProvider
+) : VectorBaseFragment() {
+
+ 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 {
+ if (isAdded) {
+ onMapReady()
+ }
+ }
+ }
+
+ override fun onPause() {
+ views.mapView.onPause()
+ super.onPause()
+ }
+
+ override fun onStop() {
+ views.mapView.onStop()
+ super.onStop()
+ }
+
+ 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)
+ }
+
+ private fun onMapReady() {
+ if (!isAdded) return
+
+ val location = args.initialLocationData ?: return
+ val userId = args.locationOwnerId
+
+ locationPinProvider.create(userId) { pinDrawable ->
+ views.mapView.apply {
+ zoomToLocation(location.latitude, location.longitude, INITIAL_MAP_ZOOM)
+ deleteAllPins()
+ addPinToMap(userId, pinDrawable)
+ updatePinLocation(userId, location.latitude, location.longitude)
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt
new file mode 100644
index 0000000000..71101d0612
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt
@@ -0,0 +1,25 @@
+/*
+ * 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 im.vector.app.core.platform.VectorViewModelAction
+
+sealed class LocationSharingAction : VectorViewModelAction {
+ data class OnLocationUpdate(val locationData: LocationData) : LocationSharingAction()
+ object OnShareLocation : LocationSharingAction()
+ object OnLocationProviderIsNotAvailable : LocationSharingAction()
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt
new file mode 100644
index 0000000000..67b36b8442
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.content.Intent
+import android.os.Parcelable
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.core.extensions.addFragment
+import im.vector.app.core.platform.VectorBaseActivity
+import im.vector.app.databinding.ActivityLocationSharingBinding
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class LocationSharingArgs(
+ val roomId: String,
+ val mode: LocationSharingMode,
+ val initialLocationData: LocationData?,
+ val locationOwnerId: String
+) : Parcelable
+
+@AndroidEntryPoint
+class LocationSharingActivity : VectorBaseActivity() {
+
+ override fun getBinding() = ActivityLocationSharingBinding.inflate(layoutInflater)
+
+ override fun initUiAndData() {
+ val locationSharingArgs: LocationSharingArgs? = intent?.extras?.getParcelable(EXTRA_LOCATION_SHARING_ARGS)
+ if (locationSharingArgs == null) {
+ finish()
+ return
+ }
+ setupToolbar(views.toolbar)
+ .setTitle(locationSharingArgs.mode.titleRes)
+ .allowBack()
+
+ if (isFirstCreation()) {
+ when (locationSharingArgs.mode) {
+ LocationSharingMode.STATIC_SHARING -> {
+ addFragment(
+ views.fragmentContainer,
+ LocationSharingFragment::class.java,
+ locationSharingArgs
+ )
+ }
+ LocationSharingMode.PREVIEW -> {
+ addFragment(
+ views.fragmentContainer,
+ LocationPreviewFragment::class.java,
+ locationSharingArgs
+ )
+ }
+ }
+ }
+ }
+
+ companion object {
+
+ private const val EXTRA_LOCATION_SHARING_ARGS = "EXTRA_LOCATION_SHARING_ARGS"
+
+ fun getIntent(context: Context, locationSharingArgs: LocationSharingArgs): Intent {
+ return Intent(context, LocationSharingActivity::class.java).apply {
+ putExtra(EXTRA_LOCATION_SHARING_ARGS, locationSharingArgs)
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt
new file mode 100644
index 0000000000..900f465f04
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.airbnb.mvrx.fragmentViewModel
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import im.vector.app.R
+import im.vector.app.core.extensions.exhaustive
+import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.databinding.FragmentLocationSharingBinding
+import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
+import org.matrix.android.sdk.api.session.Session
+import javax.inject.Inject
+
+class LocationSharingFragment @Inject constructor(
+ private val locationTracker: LocationTracker,
+ private val session: Session,
+ private val locationPinProvider: LocationPinProvider
+) : VectorBaseFragment(), LocationTracker.Callback {
+
+ init {
+ locationTracker.callback = this
+ }
+
+ private val viewModel: LocationSharingViewModel by fragmentViewModel()
+
+ private var lastZoomValue: Double = -1.0
+
+ override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationSharingBinding {
+ return FragmentLocationSharingBinding.inflate(inflater, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ views.mapView.initialize {
+ if (isAdded) {
+ onMapReady()
+ }
+ }
+
+ views.shareLocationContainer.debouncedClicks {
+ viewModel.handle(LocationSharingAction.OnShareLocation)
+ }
+
+ viewModel.observeViewEvents {
+ when (it) {
+ LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError()
+ LocationSharingViewEvents.Close -> activity?.finish()
+ }.exhaustive
+ }
+ }
+
+ override fun onPause() {
+ views.mapView.onPause()
+ super.onPause()
+ }
+
+ override fun onStop() {
+ views.mapView.onStop()
+ super.onStop()
+ }
+
+ override fun onDestroy() {
+ locationTracker.stop()
+ super.onDestroy()
+ }
+
+ private fun onMapReady() {
+ if (!isAdded) return
+
+ locationPinProvider.create(session.myUserId) {
+ views.mapView.addPinToMap(
+ pinId = USER_PIN_NAME,
+ image = it,
+ )
+ // All set, start location tracker
+ locationTracker.start()
+ }
+ }
+
+ override fun onLocationUpdate(locationData: LocationData) {
+ lastZoomValue = if (lastZoomValue == -1.0) INITIAL_MAP_ZOOM else views.mapView.getCurrentZoom() ?: INITIAL_MAP_ZOOM
+
+ views.mapView.zoomToLocation(locationData.latitude, locationData.longitude, lastZoomValue)
+ views.mapView.deleteAllPins()
+ views.mapView.updatePinLocation(USER_PIN_NAME, locationData.latitude, locationData.longitude)
+
+ viewModel.handle(LocationSharingAction.OnLocationUpdate(locationData))
+ }
+
+ override fun onLocationProviderIsNotAvailable() {
+ viewModel.handle(LocationSharingAction.OnLocationProviderIsNotAvailable)
+ }
+
+ private fun handleLocationNotAvailableError() {
+ MaterialAlertDialogBuilder(requireActivity())
+ .setTitle(R.string.location_not_available_dialog_title)
+ .setMessage(R.string.location_not_available_dialog_content)
+ .setPositiveButton(R.string.ok) { _, _ ->
+ activity?.finish()
+ }
+ .show()
+ }
+
+ companion object {
+ const val USER_PIN_NAME = "USER_PIN_NAME"
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt
new file mode 100644
index 0000000000..743daaf5e0
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt
@@ -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
+
+import im.vector.app.core.platform.VectorViewEvents
+
+sealed class LocationSharingViewEvents : VectorViewEvents {
+ object Close : LocationSharingViewEvents()
+ object LocationNotAvailableError : LocationSharingViewEvents()
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
new file mode 100644
index 0000000000..b3c97310e1
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
@@ -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.location
+
+import com.airbnb.mvrx.MavericksViewModelFactory
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.core.di.MavericksAssistedViewModelFactory
+import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.core.extensions.exhaustive
+import im.vector.app.core.platform.VectorViewModel
+import org.matrix.android.sdk.api.session.Session
+
+class LocationSharingViewModel @AssistedInject constructor(
+ @Assisted private val initialState: LocationSharingViewState,
+ session: Session
+) : VectorViewModel(initialState) {
+
+ private val room = session.getRoom(initialState.roomId)!!
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: LocationSharingViewState): LocationSharingViewModel
+ }
+
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() {
+ }
+
+ override fun handle(action: LocationSharingAction) {
+ when (action) {
+ is LocationSharingAction.OnLocationUpdate -> handleLocationUpdate(action.locationData)
+ LocationSharingAction.OnShareLocation -> handleShareLocation()
+ LocationSharingAction.OnLocationProviderIsNotAvailable -> handleLocationProviderIsNotAvailable()
+ }.exhaustive
+ }
+
+ 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)
+ }
+ }
+
+ private fun handleLocationProviderIsNotAvailable() {
+ _viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt
new file mode 100644
index 0000000000..2869929b12
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt
@@ -0,0 +1,38 @@
+/*
+ * 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 androidx.annotation.StringRes
+import com.airbnb.mvrx.MavericksState
+import im.vector.app.R
+
+enum class LocationSharingMode(@StringRes val titleRes: Int) {
+ STATIC_SHARING(R.string.location_activity_title_static_sharing),
+ PREVIEW(R.string.location_activity_title_preview)
+}
+
+data class LocationSharingViewState(
+ val roomId: String,
+ val mode: LocationSharingMode,
+ val lastKnownLocation: LocationData? = null
+) : MavericksState {
+
+ constructor(locationSharingArgs: LocationSharingArgs) : this(
+ roomId = locationSharingArgs.roomId,
+ mode = locationSharingArgs.mode
+ )
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt
new file mode 100644
index 0000000000..0c0315cf34
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.Manifest
+import android.content.Context
+import android.location.Location
+import android.location.LocationListener
+import android.location.LocationManager
+import androidx.annotation.RequiresPermission
+import androidx.core.content.getSystemService
+import timber.log.Timber
+import javax.inject.Inject
+
+class LocationTracker @Inject constructor(
+ private val context: Context
+) : LocationListener {
+
+ interface Callback {
+ fun onLocationUpdate(locationData: LocationData)
+ fun onLocationProviderIsNotAvailable()
+ }
+
+ private var locationManager: LocationManager? = null
+ var callback: Callback? = null
+
+ @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
+ fun start() {
+ val locationManager = context.getSystemService()
+
+ locationManager?.let {
+ val isGpsEnabled = it.isProviderEnabled(LocationManager.GPS_PROVIDER)
+ val isNetworkEnabled = it.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
+
+ val provider = when {
+ isGpsEnabled -> LocationManager.GPS_PROVIDER
+ isNetworkEnabled -> LocationManager.NETWORK_PROVIDER
+ else -> {
+ callback?.onLocationProviderIsNotAvailable()
+ Timber.v("## LocationTracker. There is no location provider available")
+ return
+ }
+ }
+
+ // Send last known location without waiting location updates
+ it.getLastKnownLocation(provider)?.let { lastKnownLocation ->
+ callback?.onLocationUpdate(lastKnownLocation.toLocationData())
+ }
+
+ it.requestLocationUpdates(
+ provider,
+ MIN_TIME_MILLIS_TO_UPDATE_LOCATION,
+ MIN_DISTANCE_METERS_TO_UPDATE_LOCATION,
+ this
+ )
+ } ?: run {
+ callback?.onLocationProviderIsNotAvailable()
+ Timber.v("## LocationTracker. LocationManager is not available")
+ }
+ }
+
+ @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
+ fun stop() {
+ locationManager?.removeUpdates(this)
+ callback = null
+ }
+
+ override fun onLocationChanged(location: Location) {
+ callback?.onLocationUpdate(location.toLocationData())
+ }
+
+ private fun Location.toLocationData(): LocationData {
+ return LocationData(latitude, longitude, accuracy.toDouble())
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt
new file mode 100644
index 0000000000..c64af1ebaa
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.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(onMapReady: () -> Unit) {
+ getMapAsync { map ->
+ map.setStyle(styleUrl) { style ->
+ this.symbolManager = SymbolManager(this, map, style)
+ this.map = map
+ this.style = style
+ 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}"
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt b/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt
new file mode 100644
index 0000000000..23b59bf99a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt
@@ -0,0 +1,32 @@
+/*
+ * 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 VectorMapView {
+ fun initialize(onMapReady: () -> Unit)
+
+ fun addPinToMap(pinId: String, image: Drawable)
+ fun updatePinLocation(pinId: String, latitude: Double, longitude: Double)
+ fun deleteAllPins()
+
+ fun zoomToLocation(latitude: Double, longitude: Double, zoom: Double)
+ fun getCurrentZoom(): Double?
+
+ fun onClick(callback: () -> Unit)
+}
diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
index b8b663cfcf..f66ced3299 100644
--- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
+++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
@@ -58,6 +58,10 @@ 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
import im.vector.app.features.login.LoginActivity
import im.vector.app.features.login.LoginConfig
import im.vector.app.features.matrixto.MatrixToBottomSheet
@@ -533,6 +537,18 @@ class DefaultNavigator @Inject constructor(
context.startActivity(intent)
}
+ override fun openLocationSharing(context: Context,
+ roomId: String,
+ mode: LocationSharingMode,
+ initialLocationData: LocationData?,
+ locationOwnerId: String) {
+ val intent = LocationSharingActivity.getIntent(
+ context,
+ LocationSharingArgs(roomId = roomId, mode = mode, initialLocationData = initialLocationData, locationOwnerId = locationOwnerId)
+ )
+ context.startActivity(intent)
+ }
+
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
if (buildTask) {
val stackBuilder = TaskStackBuilder.create(context)
diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
index a6d9268888..775272bd33 100644
--- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
+++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
@@ -25,6 +25,8 @@ 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
import im.vector.app.features.pin.PinMode
@@ -150,4 +152,10 @@ interface Navigator {
fun openCallTransfer(context: Context, callId: String)
fun openCreatePoll(context: Context, roomId: String, editedEventId: String?, mode: PollMode)
+
+ fun openLocationSharing(context: Context,
+ roomId: String,
+ mode: LocationSharingMode,
+ initialLocationData: LocationData?,
+ locationOwnerId: String)
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
index 611debf339..eb620f8e5c 100755
--- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
@@ -187,6 +187,9 @@ class VectorPreferences @Inject constructor(private val context: Context) {
private const val DID_ASK_TO_ENABLE_SESSION_PUSH = "DID_ASK_TO_ENABLE_SESSION_PUSH"
private const val DID_PROMOTE_NEW_RESTRICTED_JOIN_RULE = "DID_PROMOTE_NEW_RESTRICTED_JOIN_RULE"
+ // Location Sharing
+ const val SETTINGS_PREF_ENABLE_LOCATION_SHARING = "SETTINGS_PREF_ENABLE_LOCATION_SHARING"
+
private const val MEDIA_SAVING_3_DAYS = 0
private const val MEDIA_SAVING_1_WEEK = 1
private const val MEDIA_SAVING_1_MONTH = 2
@@ -196,6 +199,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE"
+ private const val SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE = "SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE"
+
// Possible values for TAKE_PHOTO_VIDEO_MODE
const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0
const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1
@@ -989,4 +994,12 @@ class VectorPreferences @Inject constructor(private val context: Context) {
putInt(TAKE_PHOTO_VIDEO_MODE, mode)
}
}
+
+ fun isLocationSharingEnabled(): Boolean {
+ return defaultPrefs.getBoolean(SETTINGS_PREF_ENABLE_LOCATION_SHARING, false) && BuildConfig.enableLocationSharing
+ }
+
+ fun labsRenderLocationsInTimeline(): Boolean {
+ return defaultPrefs.getBoolean(SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE, true)
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt
index 2e2fab06a3..50e32ae453 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt
@@ -22,6 +22,7 @@ import android.widget.CheckedTextView
import androidx.core.view.children
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.dialogs.PhotoOrVideoDialog
import im.vector.app.core.extensions.restart
@@ -149,6 +150,8 @@ class VectorSettingsPreferencesFragment @Inject constructor(
})
true
}
+
+ findPreference(VectorPreferences.SETTINGS_PREF_ENABLE_LOCATION_SHARING)?.isVisible = BuildConfig.enableLocationSharing
}
private fun updateTakePhotoOrVideoPreferenceSummary() {
diff --git a/vector/src/main/res/drawable/bg_map_user_pin.xml b/vector/src/main/res/drawable/bg_map_user_pin.xml
new file mode 100644
index 0000000000..148d3cfa29
--- /dev/null
+++ b/vector/src/main/res/drawable/bg_map_user_pin.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_attachment_location_white.xml b/vector/src/main/res/drawable/ic_attachment_location_white.xml
new file mode 100644
index 0000000000..865362312b
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_attachment_location_white.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_share_external.xml b/vector/src/main/res/drawable/ic_share_external.xml
new file mode 100644
index 0000000000..c4b78c8a83
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_share_external.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/vector/src/main/res/layout/activity_location_sharing.xml b/vector/src/main/res/layout/activity_location_sharing.xml
new file mode 100755
index 0000000000..bbb46de8c7
--- /dev/null
+++ b/vector/src/main/res/layout/activity_location_sharing.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/fragment_location_preview.xml b/vector/src/main/res/layout/fragment_location_preview.xml
new file mode 100644
index 0000000000..c2b3bdd739
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_location_preview.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/fragment_location_sharing.xml b/vector/src/main/res/layout/fragment_location_sharing.xml
new file mode 100644
index 0000000000..b9f00786de
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_location_sharing.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml
index 771d4d10f0..95e6975803 100644
--- a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml
+++ b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml
@@ -103,4 +103,18 @@
tools:text="1080 x 1024 - 43s - 12kB"
tools:visibility="visible" />
+
+
diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml
index 5a04acf677..6360b287d0 100644
--- a/vector/src/main/res/layout/item_timeline_event_base.xml
+++ b/vector/src/main/res/layout/item_timeline_event_base.xml
@@ -130,6 +130,11 @@
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_poll" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/view_attachment_type_selector.xml b/vector/src/main/res/layout/view_attachment_type_selector.xml
index 77c25572c8..e7a38ab839 100644
--- a/vector/src/main/res/layout/view_attachment_type_selector.xml
+++ b/vector/src/main/res/layout/view_attachment_type_selector.xml
@@ -72,6 +72,16 @@
android:src="@drawable/ic_attachment_poll"
app:tint="?colorPrimary" />
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 1d25d58027..c4f6abd4ac 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -2454,6 +2454,7 @@
"Gallery"
"Sticker"
Poll
+ Location
Rotate and crop
Couldn\'t handle share data
@@ -2769,6 +2770,7 @@
Poll
Reacted with: %s
Verification Conclusion
+ Shared their location
Waiting…
%s cancelled
@@ -3721,11 +3723,24 @@
Closed poll
Results are only revealed when you end the poll
+
+ Share location
+ Location
+ Share location
+ Share location
+ ${app_name} could not access your location
+ ${app_name} could not access your location. Please try again later.
+ Open with
+ Enable location sharing
+ Once enabled you will be able to send your location to any room
+ Render user locations in the timeline
+
Open camera
Send images and videos
Upload file
Send sticker
Open contacts
Create poll
+ Share location
diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml
index 7acb1d5016..d5a9a4cd3c 100644
--- a/vector/src/main/res/xml/vector_settings_labs.xml
+++ b/vector/src/main/res/xml/vector_settings_labs.xml
@@ -57,4 +57,9 @@
android:summary="@string/labs_auto_report_uisi_desc"
android:title="@string/labs_auto_report_uisi" />
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml
index 14c7dc7b80..db0830bca0 100644
--- a/vector/src/main/res/xml/vector_settings_preferences.xml
+++ b/vector/src/main/res/xml/vector_settings_preferences.xml
@@ -72,6 +72,12 @@
android:title="@string/option_take_photo_video"
tools:summary="@string/option_always_ask" />
+
+