mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Merge pull request #4817 from vector-im/feature/ons/static_location
Static Location Sharing
This commit is contained in:
commit
f14bf0dd50
59 changed files with 1548 additions and 56 deletions
1
changelog.d/2210.bugfix
Normal file
1
changelog.d/2210.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Static location sharing and rendering
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -42,6 +42,10 @@
|
|||
android:name="android.permission.WRITE_CALENDAR"
|
||||
tools:node="remove" />
|
||||
|
||||
<!-- Location Sharing -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
|
||||
<!-- Jitsi SDK is now API23+ -->
|
||||
<uses-sdk tools:overrideLibrary="org.jitsi.meet.sdk,com.oney.WebRTCModule,com.learnium.RNDeviceInfo,com.reactnativecommunity.asyncstorage,com.ocetnik.timer,com.calendarevents,com.reactnativecommunity.netinfo,com.kevinresol.react_native_default_preference,com.rnimmersive,com.corbt.keepawake,com.BV.LinearGradient,com.horcrux.svg,com.oblador.performance,com.reactnativecommunity.slider,com.brentvatne.react" />
|
||||
|
||||
|
@ -333,6 +337,7 @@
|
|||
<activity android:name=".features.spaces.people.SpacePeopleActivity" />
|
||||
<activity android:name=".features.spaces.leave.SpaceLeaveAdvancedActivity" />
|
||||
<activity android:name=".features.poll.create.CreatePollActivity" />
|
||||
<activity android:name=".features.location.LocationSharingActivity" />
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
|
|
|
@ -254,6 +254,33 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||
SOFTWARE.
|
||||
</pre>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<b>org.maplibre.gl:android-sdk</b>
|
||||
<br/>
|
||||
<b>org.maplibre.gl:android-plugin-annotation-v9</b>
|
||||
<br/>
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2021 MapLibre contributors
|
||||
|
||||
Copyright (c) 2018-2021 MapTiler.com
|
||||
|
||||
Copyright (c) 2014-2020 Mapbox
|
||||
</li>
|
||||
</ul>
|
||||
<pre>
|
||||
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.
|
||||
</pre>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<b>textdrawable</b>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<*, *>
|
||||
}
|
||||
|
|
|
@ -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<BottomSheetMessa
|
|||
@EpoxyAttribute
|
||||
var time: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var locationData: LocationData? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var locationPinProvider: LocationPinProvider? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var movementMethod: MovementMethod? = null
|
||||
|
||||
|
@ -87,6 +96,20 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
|
|||
holder.bodyDetails.setTextOrHide(bodyDetails?.charSequence)
|
||||
body.charSequence.findPillsAndProcess(coroutineScope) { it.bind(holder.body) }
|
||||
holder.timestamp.setTextOrHide(time)
|
||||
|
||||
holder.mapView.isVisible = locationData != null
|
||||
holder.body.isVisible = locationData == null
|
||||
locationData?.let { location ->
|
||||
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<BottomSheetMessa
|
|||
val bodyDetails by bind<TextView>(R.id.bottom_sheet_message_preview_body_details)
|
||||
val timestamp by bind<TextView>(R.id.bottom_sheet_message_preview_timestamp)
|
||||
val imagePreview by bind<ImageView>(R.id.bottom_sheet_message_preview_image)
|
||||
val mapView by bind<MapTilerMapView>(R.id.bottom_sheet_message_preview_location)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<String>()
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<MessageActionState>() {
|
||||
|
||||
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<MessageLocationContent>(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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<PollOptionViewState>()
|
||||
|
||||
val pollResponseSummary = informationData.pollResponseAggregatedSummary
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<String, Drawable>()
|
||||
|
||||
private val glideRequests by lazy {
|
||||
GlideApp.with(context)
|
||||
}
|
||||
|
||||
fun create(userId: String, callback: (Drawable) -> Unit) {
|
||||
if (cache.contains(userId)) {
|
||||
callback(cache[userId]!!)
|
||||
return
|
||||
}
|
||||
|
||||
activeSessionHolder.getActiveSession().getUser(userId)?.toMatrixItem()?.let {
|
||||
val size = dimensionConverter.dpToPx(44)
|
||||
avatarRenderer.render(glideRequests, it, object : CustomTarget<Drawable>(size, size) {
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
val bgUserPin = ContextCompat.getDrawable(context, R.drawable.bg_map_user_pin)!!
|
||||
val layerDrawable = LayerDrawable(arrayOf(bgUserPin, resource))
|
||||
val horizontalInset = 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?
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<MessageLocationItem.Holder>() {
|
||||
|
||||
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<ConstraintLayout>(R.id.mapViewContainer)
|
||||
val mapView by bind<MapTilerMapView>(R.id.mapView)
|
||||
val clickableMapArea by bind<FrameLayout>(R.id.clickableMapArea)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageContentLocationStub
|
||||
private const val INITIAL_ZOOM = 15.0
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<FragmentLocationPreviewBinding>() {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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<ActivityLocationSharingBinding>() {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<FragmentLocationSharingBinding>(), 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"
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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<LocationSharingViewState, LocationSharingAction, LocationSharingViewEvents>(initialState) {
|
||||
|
||||
private val room = session.getRoom(initialState.roomId)!!
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<LocationSharingViewModel, LocationSharingViewState> {
|
||||
override fun create(initialState: LocationSharingViewState): LocationSharingViewModel
|
||||
}
|
||||
|
||||
companion object : MavericksViewModelFactory<LocationSharingViewModel, LocationSharingViewState> 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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>()
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -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}"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<VectorSwitchPreference>(VectorPreferences.SETTINGS_PREF_ENABLE_LOCATION_SHARING)?.isVisible = BuildConfig.enableLocationSharing
|
||||
}
|
||||
|
||||
private fun updateTakePhotoOrVideoPreferenceSummary() {
|
||||
|
|
10
vector/src/main/res/drawable/bg_map_user_pin.xml
Normal file
10
vector/src/main/res/drawable/bg_map_user_pin.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="51dp"
|
||||
android:height="55dp"
|
||||
android:viewportWidth="51"
|
||||
android:viewportHeight="55">
|
||||
<path
|
||||
android:pathData="M29.1957,50.7341C41.5276,48.9438 51,38.3281 51,25.5C51,11.4167 39.5833,0 25.5,0C11.4167,0 0,11.4167 0,25.5C0,38.3282 9.4725,48.9439 21.8045,50.7342L25.5001,54.2903L29.1957,50.7341Z"
|
||||
android:fillColor="#0DBD8B"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="14dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="14"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M7,0C3.13,0 0,3.2152 0,7.1905C0,11.4741 4.42,17.3806 6.24,19.6302C6.64,20.1233 7.37,20.1233 7.77,19.6302C9.58,17.3806 14,11.4741 14,7.1905C14,3.2152 10.87,0 7,0ZM7,9.7586C5.62,9.7586 4.5,8.6081 4.5,7.1905C4.5,5.773 5.62,4.6225 7,4.6225C8.38,4.6225 9.5,5.773 9.5,7.1905C9.5,8.6081 8.38,9.7586 7,9.7586Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
5
vector/src/main/res/drawable/ic_share_external.xml
Normal file
5
vector/src/main/res/drawable/ic_share_external.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#0DBD8B" android:fillType="evenOdd" android:pathData="M21.768,2.8608V3.0305C21.7809,3.0862 21.7889,3.143 21.792,3.2002V8.4608C21.792,8.718 21.6908,8.9646 21.5108,9.1465C21.3308,9.3283 21.0866,9.4305 20.832,9.4305C20.5774,9.4305 20.3332,9.3283 20.1531,9.1465C19.9731,8.9646 19.872,8.718 19.872,8.4608V5.5517L11.256,14.2547C11.1661,14.3455 11.0595,14.4174 10.9421,14.4665C10.8248,14.5156 10.699,14.5409 10.572,14.5409C10.4449,14.5409 10.3192,14.5156 10.2018,14.4665C10.0844,14.4174 9.9778,14.3455 9.888,14.2547C9.7982,14.164 9.7269,14.0563 9.6783,13.9377C9.6297,13.8192 9.6046,13.6921 9.6046,13.5638C9.6046,13.4355 9.6297,13.3085 9.6783,13.1899C9.7269,13.0714 9.7982,12.9636 9.888,12.8729L18.504,4.2426H15.624C15.4979,4.2426 15.3731,4.2175 15.2566,4.1688C15.1401,4.1201 15.0343,4.0486 14.9451,3.9586C14.856,3.8686 14.7853,3.7617 14.737,3.644C14.6888,3.5264 14.664,3.4003 14.664,3.2729C14.664,3.1456 14.6888,3.0195 14.737,2.9018C14.7853,2.7842 14.856,2.6773 14.9451,2.5872C15.0343,2.4972 15.1401,2.4258 15.2566,2.377C15.3731,2.3283 15.4979,2.3032 15.624,2.3032H21.192L21.288,2.3517L21.36,2.4002L21.552,2.5457L21.672,2.6911L21.72,2.7638L21.768,2.8608ZM16.464,22.0122H5.088C4.3242,22.0122 3.5917,21.7057 3.0515,21.1602C2.5114,20.6146 2.208,19.8747 2.208,19.1031V7.6122C2.208,6.8407 2.5114,6.1007 3.0515,5.5552C3.5917,5.0096 4.3242,4.7031 5.088,4.7031H11.88C12.1346,4.7031 12.3788,4.8053 12.5588,4.9871C12.7389,5.169 12.84,5.4156 12.84,5.6728C12.84,5.93 12.7389,6.1767 12.5588,6.3585C12.3788,6.5403 12.1346,6.6425 11.88,6.6425H5.088C4.8334,6.6425 4.5892,6.7447 4.4092,6.9265C4.2291,7.1084 4.128,7.355 4.128,7.6122V19.1031C4.128,19.3603 4.2291,19.607 4.4092,19.7888C4.5892,19.9707 4.8334,20.0728 5.088,20.0728H16.464C16.7186,20.0728 16.9628,19.9707 17.1428,19.7888C17.3229,19.607 17.424,19.3603 17.424,19.1031V12.2425C17.424,11.9853 17.5252,11.7387 17.7052,11.5568C17.8852,11.375 18.1294,11.2728 18.384,11.2728C18.6386,11.2728 18.8828,11.375 19.0628,11.5568C19.2429,11.7387 19.344,11.9853 19.344,12.2425V19.1031C19.344,19.8747 19.0406,20.6146 18.5005,21.1602C17.9604,21.7057 17.2278,22.0122 16.464,22.0122Z"/>
|
||||
</vector>
|
24
vector/src/main/res/layout/activity_location_sharing.xml
Executable file
24
vector/src/main/res/layout/activity_location_sharing.xml
Executable file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBarLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
13
vector/src/main/res/layout/fragment_location_preview.xml
Normal file
13
vector/src/main/res/layout/fragment_location_preview.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<im.vector.app.features.location.MapTilerMapView
|
||||
android:id="@+id/mapView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:mapbox_renderTextureMode="true" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
50
vector/src/main/res/layout/fragment_location_sharing.xml
Normal file
50
vector/src/main/res/layout/fragment_location_sharing.xml
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<im.vector.app.features.location.MapTilerMapView
|
||||
android:id="@+id/mapView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:mapbox_renderTextureMode="true" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/shareLocationContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="72dp"
|
||||
android:background="?android:colorBackground"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/shareLocationImageView"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:background="@drawable/circle"
|
||||
android:backgroundTint="?colorPrimary"
|
||||
android:contentDescription="@string/a11y_location_share_icon"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/ic_attachment_location_white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Vector.Subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/location_share"
|
||||
android:textColor="?colorPrimary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/shareLocationImageView"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -103,4 +103,18 @@
|
|||
tools:text="1080 x 1024 - 43s - 12kB"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<im.vector.app.features.location.MapTilerMapView
|
||||
android:id="@+id/bottom_sheet_message_preview_location"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:contentDescription="@string/attachment_type_location"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="@id/bottom_sheet_message_preview_timestamp"
|
||||
app:layout_constraintStart_toStartOf="@id/bottom_sheet_message_preview_sender"
|
||||
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
|
||||
app:mapbox_renderTextureMode="true"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -130,6 +130,11 @@
|
|||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout="@layout/item_timeline_event_poll" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentLocationStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout="@layout/item_timeline_event_location_stub" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<im.vector.app.core.ui.views.SendStateImageView
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="8dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/mapViewContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<im.vector.app.features.location.MapTilerMapView
|
||||
android:id="@+id/mapView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="200dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:mapbox_renderTextureMode="true" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/clickableMapArea"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/mapView"
|
||||
app:layout_constraintEnd_toEndOf="@id/mapView"
|
||||
app:layout_constraintStart_toStartOf="@id/mapView"
|
||||
app:layout_constraintTop_toTopOf="@id/mapView" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
|
@ -72,6 +72,16 @@
|
|||
android:src="@drawable/ic_attachment_poll"
|
||||
app:tint="?colorPrimary" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/attachmentLocationButton"
|
||||
android:layout_width="@dimen/layout_touch_size"
|
||||
android:layout_height="@dimen/layout_touch_size"
|
||||
android:layout_marginStart="2dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:contentDescription="@string/attachment_type_location"
|
||||
android:src="@drawable/ic_attachment_location"
|
||||
app:tint="?colorPrimary" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/attachmentCameraButton"
|
||||
android:layout_width="@dimen/layout_touch_size"
|
||||
|
|
12
vector/src/main/res/menu/menu_location_preview.xml
Normal file
12
vector/src/main/res/menu/menu_location_preview.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/share_external"
|
||||
android:icon="@drawable/ic_share_external"
|
||||
android:title="@string/location_share_external"
|
||||
app:iconTint="?colorPrimary"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
|
@ -2454,6 +2454,7 @@
|
|||
<string name="attachment_type_gallery">"Gallery"</string>
|
||||
<string name="attachment_type_sticker">"Sticker"</string>
|
||||
<string name="attachment_type_poll">Poll</string>
|
||||
<string name="attachment_type_location">Location</string>
|
||||
<string name="rotate_and_crop_screen_title">Rotate and crop</string>
|
||||
<string name="error_handling_incoming_share">Couldn\'t handle share data</string>
|
||||
|
||||
|
@ -2769,6 +2770,7 @@
|
|||
<string name="sent_a_poll">Poll</string>
|
||||
<string name="sent_a_reaction">Reacted with: %s</string>
|
||||
<string name="sent_verification_conclusion">Verification Conclusion</string>
|
||||
<string name="sent_location">Shared their location</string>
|
||||
|
||||
<string name="verification_request_waiting">Waiting…</string>
|
||||
<string name="verification_request_other_cancelled">%s cancelled</string>
|
||||
|
@ -3721,11 +3723,24 @@
|
|||
<string name="closed_poll_option_title">Closed poll</string>
|
||||
<string name="closed_poll_option_description">Results are only revealed when you end the poll</string>
|
||||
|
||||
<!-- Location -->
|
||||
<string name="location_activity_title_static_sharing">Share location</string>
|
||||
<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="template_location_not_available_dialog_title">${app_name} could not access your location</string>
|
||||
<string name="template_location_not_available_dialog_content">${app_name} could not access your location. Please try again later.</string>
|
||||
<string name="location_share_external">Open with</string>
|
||||
<string name="settings_enable_location_sharing">Enable location sharing</string>
|
||||
<string name="settings_enable_location_sharing_summary">Once enabled you will be able to send your location to any room</string>
|
||||
<string name="labs_render_locations_in_timeline">Render user locations in the timeline</string>
|
||||
|
||||
<string name="tooltip_attachment_photo">Open camera</string>
|
||||
<string name="tooltip_attachment_gallery">Send images and videos</string>
|
||||
<string name="tooltip_attachment_file">Upload file</string>
|
||||
<string name="tooltip_attachment_sticker">Send sticker</string>
|
||||
<string name="tooltip_attachment_contact">Open contacts</string>
|
||||
<string name="tooltip_attachment_poll">Create poll</string>
|
||||
<string name="tooltip_attachment_location">Share location</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -57,4 +57,9 @@
|
|||
android:summary="@string/labs_auto_report_uisi_desc"
|
||||
android:title="@string/labs_auto_report_uisi" />
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:key="SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE"
|
||||
android:title="@string/labs_render_locations_in_timeline" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
|
@ -72,6 +72,12 @@
|
|||
android:title="@string/option_take_photo_video"
|
||||
tools:summary="@string/option_always_ask" />
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="SETTINGS_PREF_ENABLE_LOCATION_SHARING"
|
||||
android:summary="@string/settings_enable_location_sharing_summary"
|
||||
android:title="@string/settings_enable_location_sharing" />
|
||||
|
||||
</im.vector.app.core.preference.VectorPreferenceCategory>
|
||||
|
||||
<im.vector.app.core.preference.VectorPreferenceCategory android:title="@string/settings_category_timeline">
|
||||
|
|
Loading…
Reference in a new issue