From 077977b8bf4e5423114a603b5e1f779deb42d1af Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 2 May 2022 12:18:21 +0200 Subject: [PATCH] Show running live state item --- .../LiveLocationShareMessageItemFactory.kt | 43 +++++++++++++++++-- .../timeline/item/MessageLiveLocationItem.kt | 30 ++++++++++--- .../app/features/location/LocationData.kt | 11 ++++- .../live/LocationLiveMessageBannerView.kt | 24 ++++++----- 4 files changed, 88 insertions(+), 20 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt index 760ec92cd6..5d9742e1c6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt @@ -16,24 +16,36 @@ package im.vector.app.features.home.room.detail.timeline.factory +import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.DateProvider import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.LiveLocationShareSummaryData +import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationItem +import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem_ +import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE +import im.vector.app.features.location.UrlMapProvider +import im.vector.app.features.location.toLocationData import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.Session import org.threeten.bp.LocalDateTime import timber.log.Timber import javax.inject.Inject class LiveLocationShareMessageItemFactory @Inject constructor( + private val session: Session, private val dimensionConverter: DimensionConverter, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val avatarSizeProvider: AvatarSizeProvider, + private val urlMapProvider: UrlMapProvider, + private val locationPinProvider: LocationPinProvider, + private val vectorDateFormatter: VectorDateFormatter, ) { fun create( @@ -41,10 +53,10 @@ class LiveLocationShareMessageItemFactory @Inject constructor( highlight: Boolean, attributes: AbsMessageItem.Attributes, ): VectorEpoxyModel<*>? { - return when (getViewState(liveLocationShareSummaryData)) { + return when (val currentState = getViewState(liveLocationShareSummaryData)) { LiveLocationShareViewState.Loading -> buildLoadingItem(highlight, attributes) LiveLocationShareViewState.Inactive -> buildInactiveItem() - is LiveLocationShareViewState.Running -> buildRunningItem() + is LiveLocationShareViewState.Running -> buildRunningItem(highlight, attributes, currentState) LiveLocationShareViewState.Unkwown -> null } } @@ -64,7 +76,32 @@ class LiveLocationShareMessageItemFactory @Inject constructor( .leftGuideline(avatarSizeProvider.leftGuideline) } - private fun buildRunningItem() = null + private fun buildRunningItem( + highlight: Boolean, + attributes: AbsMessageItem.Attributes, + runningState: LiveLocationShareViewState.Running, + ): MessageLiveLocationItem { + // TODO only render location if enabled in preferences: to be handled in a next PR + val width = timelineMediaSizeProvider.getMaxSize().first + val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP) + + val locationUrl = runningState.lastGeoUri.toLocationData()?.let { + urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height) + } + + return MessageLiveLocationItem_() + .attributes(attributes) + .locationUrl(locationUrl) + .mapWidth(width) + .mapHeight(height) + .locationUserId(attributes.informationData.senderId) + .locationPinProvider(locationPinProvider) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + .currentUserId(session.myUserId) + .endOfLiveDateTime(runningState.endOfLiveDateTime) + .vectorDateFormatter(vectorDateFormatter) + } private fun buildInactiveItem() = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt index bbf2ca1348..d81e26f29b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt @@ -20,30 +20,42 @@ import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R +import im.vector.app.core.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.resources.toTimestamp import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.location.live.LocationLiveMessageBannerView import im.vector.app.features.location.live.LocationLiveMessageBannerViewState +import org.threeten.bp.LocalDateTime @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageLiveLocationItem : AbsMessageLocationItem() { - // TODO define the needed attributes @EpoxyAttribute var currentUserId: String? = null + @EpoxyAttribute + var endOfLiveDateTime: LocalDateTime? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + lateinit var vectorDateFormatter: VectorDateFormatter + override fun bind(holder: Holder) { super.bind(holder) bindLocationLiveBanner(holder) } private fun bindLocationLiveBanner(holder: Holder) { - // TODO add check on device id to confirm that is the one that sent the beacon + // TODO in a future PR add check on device id to confirm that is the one that sent the beacon val isEmitter = currentUserId != null && currentUserId == locationUserId val messageLayout = attributes.informationData.messageLayout val viewState = buildViewState(holder, messageLayout, isEmitter) holder.locationLiveMessageBanner.isVisible = true holder.locationLiveMessageBanner.render(viewState) + holder.locationLiveMessageBanner.stopButton.setOnClickListener { + // TODO call stop live location + } // TODO adjust Copyright map placement if needed } @@ -55,7 +67,7 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem LocationLiveMessageBannerViewState.Emitter( - remainingTimeInMillis = 4000 * 1000L, + remainingTimeInMillis = getRemainingTimeOfLiveInMillis(), bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius, bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius, isStopButtonCenteredVertically = false @@ -64,12 +76,12 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem { val cornerRadius = getBannerCornerRadiusForDefaultLayout(holder) LocationLiveMessageBannerViewState.Emitter( - remainingTimeInMillis = 4000 * 1000L, + remainingTimeInMillis = getRemainingTimeOfLiveInMillis(), bottomStartCornerRadiusInDp = cornerRadius, bottomEndCornerRadiusInDp = cornerRadius, isStopButtonCenteredVertically = true @@ -80,7 +92,7 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem(R.id.locationLiveMessageBanner) } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationData.kt b/vector/src/main/java/im/vector/app/features/location/LocationData.kt index a69d8d20e3..96d544ba5a 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationData.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationData.kt @@ -29,7 +29,7 @@ data class LocationData( ) : Parcelable /** - * Creates location data from a LocationContent + * Creates location data from a MessageLocationContent * "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30) * @return location data or null if geo uri is not valid */ @@ -37,6 +37,15 @@ fun MessageLocationContent.toLocationData(): LocationData? { return parseGeo(getBestGeoUri()) } +/** + * Creates location data from a geoUri String + * "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30) + * @return location data or null if geo uri is null or not valid + */ +fun String?.toLocationData(): LocationData? { + return this?.let { parseGeo(it) } +} + @VisibleForTesting fun parseGeo(geo: String): LocationData? { val geoParts = geo diff --git a/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerView.kt b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerView.kt index 3c2a13655b..7d008e0fc6 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerView.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerView.kt @@ -98,17 +98,21 @@ class LocationLiveMessageBannerView @JvmOverloads constructor( title.text = context.getString(R.string.location_share_live_enabled) countDownTimer?.cancel() - countDownTimer = object : CountDownTimer(viewState.remainingTimeInMillis, REMAINING_TIME_COUNTER_INTERVAL_IN_MS) { - override fun onTick(millisUntilFinished: Long) { - val duration = Duration.ofMillis(millisUntilFinished.coerceAtLeast(0L)) - subTitle.text = context.getString(R.string.location_share_live_remaining_time, TextUtils.formatDurationWithUnits(context, duration)) - } + viewState.remainingTimeInMillis + .takeIf { it >= 0 } + ?.let { + countDownTimer = object : CountDownTimer(it, REMAINING_TIME_COUNTER_INTERVAL_IN_MS) { + override fun onTick(millisUntilFinished: Long) { + val duration = Duration.ofMillis(millisUntilFinished.coerceAtLeast(0L)) + subTitle.text = context.getString(R.string.location_share_live_remaining_time, TextUtils.formatDurationWithUnits(context, duration)) + } - override fun onFinish() { - subTitle.text = context.getString(R.string.location_share_live_remaining_time, TextUtils.formatDurationWithUnits(context, Duration.ofMillis(0L))) - } - } - countDownTimer?.start() + override fun onFinish() { + subTitle.text = context.getString(R.string.location_share_live_remaining_time, TextUtils.formatDurationWithUnits(context, Duration.ofMillis(0L))) + } + } + countDownTimer?.start() + } val rootLayout: ConstraintLayout? = (binding.root as? ConstraintLayout) rootLayout?.let { parentLayout ->