Merge pull request #6712 from vector-im/feature/mna/map-loading-error

[Location Share] Render fallback UI when map fails to load (PSG-607)
This commit is contained in:
Maxime NATUREL 2022-08-05 11:17:37 +02:00 committed by GitHub
commit 2dc92caa30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 398 additions and 52 deletions

1
changelog.d/6711.feature Normal file
View file

@ -0,0 +1 @@
[Location Share] Render fallback UI when map fails to load

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MapLoadingErrorView">
<attr name="mapErrorDescription" format="string" />
</declare-styleable>
</resources>

View file

@ -64,8 +64,8 @@ import im.vector.app.features.home.room.detail.search.SearchFragment
import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.list.RoomListFragment
import im.vector.app.features.home.room.list.home.HomeRoomListFragment import im.vector.app.features.home.room.list.home.HomeRoomListFragment
import im.vector.app.features.home.room.threads.list.views.ThreadListFragment import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
import im.vector.app.features.location.LocationPreviewFragment
import im.vector.app.features.location.LocationSharingFragment import im.vector.app.features.location.LocationSharingFragment
import im.vector.app.features.location.preview.LocationPreviewFragment
import im.vector.app.features.login.LoginCaptchaFragment import im.vector.app.features.login.LoginCaptchaFragment
import im.vector.app.features.login.LoginFragment import im.vector.app.features.login.LoginFragment
import im.vector.app.features.login.LoginGenericTextInputFormFragment import im.vector.app.features.login.LoginGenericTextInputFormFragment

View file

@ -56,6 +56,7 @@ import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
import im.vector.app.features.invite.InviteUsersToRoomViewModel import im.vector.app.features.invite.InviteUsersToRoomViewModel
import im.vector.app.features.location.LocationSharingViewModel import im.vector.app.features.location.LocationSharingViewModel
import im.vector.app.features.location.live.map.LiveLocationMapViewModel import im.vector.app.features.location.live.map.LiveLocationMapViewModel
import im.vector.app.features.location.preview.LocationPreviewViewModel
import im.vector.app.features.login.LoginViewModel import im.vector.app.features.login.LoginViewModel
import im.vector.app.features.login2.LoginViewModel2 import im.vector.app.features.login2.LoginViewModel2
import im.vector.app.features.login2.created.AccountCreatedViewModel import im.vector.app.features.login2.created.AccountCreatedViewModel
@ -605,6 +606,11 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(LocationSharingViewModel::class) @MavericksViewModelKey(LocationSharingViewModel::class)
fun createLocationSharingViewModelFactory(factory: LocationSharingViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun createLocationSharingViewModelFactory(factory: LocationSharingViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(LocationPreviewViewModel::class)
fun createLocationPreviewViewModelFactory(factory: LocationPreviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds @Binds
@IntoMap @IntoMap
@MavericksViewModelKey(VectorAttachmentViewerViewModel::class) @MavericksViewModelKey(VectorAttachmentViewerViewModel::class)

View file

@ -36,6 +36,8 @@ import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners
import im.vector.app.features.location.MapLoadingErrorView
import im.vector.app.features.location.MapLoadingErrorViewState
abstract class AbsMessageLocationItem<H : AbsMessageLocationItem.Holder>( abstract class AbsMessageLocationItem<H : AbsMessageLocationItem.Holder>(
@LayoutRes layoutId: Int = R.layout.item_timeline_event_base @LayoutRes layoutId: Int = R.layout.item_timeline_event_base
@ -86,8 +88,10 @@ abstract class AbsMessageLocationItem<H : AbsMessageLocationItem.Holder>(
target: Target<Drawable>?, target: Target<Drawable>?,
isFirstResource: Boolean isFirstResource: Boolean
): Boolean { ): Boolean {
holder.staticMapPinImageView.setImageResource(R.drawable.ic_location_pin_failed) holder.staticMapPinImageView.setImageDrawable(null)
holder.staticMapErrorTextView.isVisible = true holder.staticMapLoadingErrorView.isVisible = true
val mapErrorViewState = MapLoadingErrorViewState(imageCornerTransformation)
holder.staticMapLoadingErrorView.render(mapErrorViewState)
holder.staticMapCopyrightTextView.isVisible = false holder.staticMapCopyrightTextView.isVisible = false
return false return false
} }
@ -103,7 +107,7 @@ abstract class AbsMessageLocationItem<H : AbsMessageLocationItem.Holder>(
// we are not using Glide since it does not display it correctly when there is no user photo // we are not using Glide since it does not display it correctly when there is no user photo
holder.staticMapPinImageView.setImageDrawable(pinDrawable) holder.staticMapPinImageView.setImageDrawable(pinDrawable)
} }
holder.staticMapErrorTextView.isVisible = false holder.staticMapLoadingErrorView.isVisible = false
holder.staticMapCopyrightTextView.isVisible = true holder.staticMapCopyrightTextView.isVisible = true
return false return false
} }
@ -115,7 +119,7 @@ abstract class AbsMessageLocationItem<H : AbsMessageLocationItem.Holder>(
abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) { abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) {
val staticMapImageView by bind<ImageView>(R.id.staticMapImageView) val staticMapImageView by bind<ImageView>(R.id.staticMapImageView)
val staticMapPinImageView by bind<ImageView>(R.id.staticMapPinImageView) val staticMapPinImageView by bind<ImageView>(R.id.staticMapPinImageView)
val staticMapErrorTextView by bind<TextView>(R.id.staticMapErrorTextView) val staticMapLoadingErrorView by bind<MapLoadingErrorView>(R.id.staticMapLoadingError)
val staticMapCopyrightTextView by bind<TextView>(R.id.staticMapCopyrightTextView) val staticMapCopyrightTextView by bind<TextView>(R.id.staticMapCopyrightTextView)
} }
} }

View file

@ -25,4 +25,5 @@ sealed class LocationSharingAction : VectorViewModelAction {
object ZoomToUserLocation : LocationSharingAction() object ZoomToUserLocation : LocationSharingAction()
object LiveLocationSharingRequested : LocationSharingAction() object LiveLocationSharingRequested : LocationSharingAction()
data class StartLiveLocationSharing(val durationMillis: Long) : LocationSharingAction() data class StartLiveLocationSharing(val durationMillis: Long) : LocationSharingAction()
object ShowMapLoadingError : LocationSharingAction()
} }

View file

@ -23,6 +23,7 @@ import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityLocationSharingBinding import im.vector.app.databinding.ActivityLocationSharingBinding
import im.vector.app.features.location.preview.LocationPreviewFragment
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize

View file

@ -24,6 +24,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
@ -69,6 +70,7 @@ class LocationSharingFragment @Inject constructor(
private var mapView: WeakReference<MapView>? = null private var mapView: WeakReference<MapView>? = null
private var hasRenderedUserAvatar = false private var hasRenderedUserAvatar = false
private var mapLoadingErrorListener: MapView.OnDidFailLoadingMapListener? = null
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationSharingBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationSharingBinding {
return FragmentLocationSharingBinding.inflate(inflater, container, false) return FragmentLocationSharingBinding.inflate(inflater, container, false)
@ -87,6 +89,9 @@ class LocationSharingFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
mapView = WeakReference(views.mapView) mapView = WeakReference(views.mapView)
mapLoadingErrorListener = MapView.OnDidFailLoadingMapListener {
viewModel.handle(LocationSharingAction.ShowMapLoadingError)
}.also { views.mapView.addOnDidFailLoadingMapListener(it) }
views.mapView.onCreate(savedInstanceState) views.mapView.onCreate(savedInstanceState)
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
@ -112,6 +117,12 @@ class LocationSharingFragment @Inject constructor(
} }
} }
override fun onDestroyView() {
mapLoadingErrorListener?.let { mapView?.get()?.removeOnDidFailLoadingMapListener(it) }
mapLoadingErrorListener = null
super.onDestroyView()
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
views.mapView.onResume() views.mapView.onResume()
@ -256,20 +267,27 @@ class LocationSharingFragment @Inject constructor(
} }
private fun updateMap(state: LocationSharingViewState) { private fun updateMap(state: LocationSharingViewState) {
// first, update the options view if (state.loadingMapHasFailed) {
val options: Set<LocationSharingOption> = when (state.areTargetAndUserLocationEqual) { views.shareLocationOptionsPicker.render(emptySet())
true -> setOf(LocationSharingOption.USER_CURRENT, LocationSharingOption.USER_LIVE) views.shareLocationMapLoadingError.isVisible = true
false -> setOf(LocationSharingOption.PINNED) } else {
else -> emptySet() // first, update the options view
} val options: Set<LocationSharingOption> = when (state.areTargetAndUserLocationEqual) {
views.shareLocationOptionsPicker.render(options) true -> setOf(LocationSharingOption.USER_CURRENT, LocationSharingOption.USER_LIVE)
false -> setOf(LocationSharingOption.PINNED)
else -> emptySet()
}
views.shareLocationOptionsPicker.render(options)
// then, update the map using the height of the options view after it has been rendered // then, update the map using the height of the options view after it has been rendered
views.shareLocationOptionsPicker.post { views.shareLocationOptionsPicker.post {
val mapState = state val mapState = state
.toMapState() .toMapState()
.copy(logoMarginBottom = views.shareLocationOptionsPicker.height) .copy(logoMarginBottom = views.shareLocationOptionsPicker.height)
views.mapView.render(mapState) views.mapView.render(mapState)
}
views.shareLocationMapLoadingError.isGone = true
} }
} }

View file

@ -152,6 +152,7 @@ class LocationSharingViewModel @AssistedInject constructor(
LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction() LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction()
LocationSharingAction.LiveLocationSharingRequested -> handleLiveLocationSharingRequestedAction() LocationSharingAction.LiveLocationSharingRequested -> handleLiveLocationSharingRequestedAction()
is LocationSharingAction.StartLiveLocationSharing -> handleStartLiveLocationSharingAction(action.durationMillis) is LocationSharingAction.StartLiveLocationSharing -> handleStartLiveLocationSharingAction(action.durationMillis)
LocationSharingAction.ShowMapLoadingError -> handleShowMapLoadingError()
} }
} }
@ -211,6 +212,10 @@ class LocationSharingViewModel @AssistedInject constructor(
) )
} }
private fun handleShowMapLoadingError() {
setState { copy(loadingMapHasFailed = true) }
}
private fun onLocationUpdate(locationData: LocationData) { private fun onLocationUpdate(locationData: LocationData) {
Timber.d("onLocationUpdate()") Timber.d("onLocationUpdate()")
setState { setState {

View file

@ -36,6 +36,7 @@ data class LocationSharingViewState(
val lastKnownUserLocation: LocationData? = null, val lastKnownUserLocation: LocationData? = null,
val locationTargetDrawable: Drawable? = null, val locationTargetDrawable: Drawable? = null,
val canShareLiveLocation: Boolean = false, val canShareLiveLocation: Boolean = false,
val loadingMapHasFailed: Boolean = false
) : MavericksState { ) : MavericksState {
constructor(locationSharingArgs: LocationSharingArgs) : this( constructor(locationSharingArgs: LocationSharingArgs) : this(

View file

@ -0,0 +1,69 @@
/*
* 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
import android.content.Context
import android.content.res.TypedArray
import android.graphics.drawable.ColorDrawable
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.res.use
import im.vector.app.R
import im.vector.app.core.glide.GlideApp
import im.vector.app.databinding.ViewMapLoadingErrorBinding
import im.vector.app.features.themes.ThemeUtils
/**
* Custom view to display an error when map fails to load.
*/
class MapLoadingErrorView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private val binding = ViewMapLoadingErrorBinding.inflate(
LayoutInflater.from(context),
this
)
init {
context.obtainStyledAttributes(
attrs,
R.styleable.MapLoadingErrorView,
0,
0
).use {
setErrorDescription(it)
}
}
private fun setErrorDescription(typedArray: TypedArray) {
val description = typedArray.getString(R.styleable.MapLoadingErrorView_mapErrorDescription)
if (description.isNullOrEmpty()) {
binding.mapLoadingErrorDescription.setText(R.string.location_share_loading_map_error)
} else {
binding.mapLoadingErrorDescription.text = description
}
}
fun render(mapLoadingErrorViewState: MapLoadingErrorViewState) {
GlideApp.with(binding.mapLoadingErrorBackground)
.load(ColorDrawable(ThemeUtils.getColor(context, R.attr.vctr_system)))
.transform(mapLoadingErrorViewState.backgroundTransformation)
.into(binding.mapLoadingErrorBackground)
}
}

View file

@ -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
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
data class MapLoadingErrorViewState(val backgroundTransformation: BitmapTransformation)

View file

@ -22,4 +22,5 @@ sealed class LiveLocationMapAction : VectorViewModelAction {
data class AddMapSymbol(val key: String, val value: Long) : LiveLocationMapAction() data class AddMapSymbol(val key: String, val value: Long) : LiveLocationMapAction()
data class RemoveMapSymbol(val key: String) : LiveLocationMapAction() data class RemoveMapSymbol(val key: String) : LiveLocationMapAction()
object StopSharing : LiveLocationMapAction() object StopSharing : LiveLocationMapAction()
object ShowMapLoadingError : LiveLocationMapAction()
} }

View file

@ -71,11 +71,13 @@ class LiveLocationMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
private val viewModel: LiveLocationMapViewModel by fragmentViewModel() private val viewModel: LiveLocationMapViewModel by fragmentViewModel()
private var mapboxMap: WeakReference<MapboxMap>? = null private var mapboxMap: WeakReference<MapboxMap>? = null
private var mapView: MapView? = null
private var symbolManager: SymbolManager? = null private var symbolManager: SymbolManager? = null
private var mapStyle: Style? = null private var mapStyle: Style? = null
private val pendingLiveLocations = mutableListOf<UserLiveLocationViewState>() private val pendingLiveLocations = mutableListOf<UserLiveLocationViewState>()
private var isMapFirstUpdate = true private var isMapFirstUpdate = true
private var onSymbolClickListener: OnSymbolClickListener? = null private var onSymbolClickListener: OnSymbolClickListener? = null
private var mapLoadingErrorListener: MapView.OnDidFailLoadingMapListener? = null
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLiveLocationMapViewBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLiveLocationMapViewBinding {
return FragmentLiveLocationMapViewBinding.inflate(layoutInflater, container, false) return FragmentLiveLocationMapViewBinding.inflate(layoutInflater, container, false)
@ -84,6 +86,7 @@ class LiveLocationMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
observeViewEvents() observeViewEvents()
setupMap()
views.liveLocationBottomSheetRecyclerView.configureWith(bottomSheetController, hasFixedSize = false, disableItemAnimation = true) views.liveLocationBottomSheetRecyclerView.configureWith(bottomSheetController, hasFixedSize = false, disableItemAnimation = true)
@ -106,22 +109,24 @@ class LiveLocationMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
} }
} }
override fun onResume() {
super.onResume()
setupMap()
}
override fun onDestroyView() { override fun onDestroyView() {
onSymbolClickListener?.let { symbolManager?.removeClickListener(it) } onSymbolClickListener?.let { symbolManager?.removeClickListener(it) }
symbolManager?.onDestroy() symbolManager?.onDestroy()
bottomSheetController.callback = null bottomSheetController.callback = null
views.liveLocationBottomSheetRecyclerView.cleanup() views.liveLocationBottomSheetRecyclerView.cleanup()
mapLoadingErrorListener?.let { mapView?.removeOnDidFailLoadingMapListener(it) }
mapLoadingErrorListener = null
mapView = null
super.onDestroyView() super.onDestroyView()
} }
private fun setupMap() { private fun setupMap() {
val mapFragment = getOrCreateSupportMapFragment() val mapFragment = getOrCreateSupportMapFragment()
mapFragment.getMapAsync { mapboxMap -> mapFragment.getMapAsync { mapboxMap ->
(mapFragment.view as? MapView)?.let {
mapView = it
listenMapLoadingError(it)
}
lifecycleScope.launch { lifecycleScope.launch {
mapboxMap.setStyle(urlMapProvider.getMapUrl()) { style -> mapboxMap.setStyle(urlMapProvider.getMapUrl()) { style ->
mapStyle = style mapStyle = style
@ -141,6 +146,12 @@ class LiveLocationMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
} }
} }
private fun listenMapLoadingError(mapView: MapView) {
mapLoadingErrorListener = MapView.OnDidFailLoadingMapListener {
viewModel.handle(LiveLocationMapAction.ShowMapLoadingError)
}.also { mapView.addOnDidFailLoadingMapListener(it) }
}
private fun onSymbolClicked(symbol: Symbol?) { private fun onSymbolClicked(symbol: Symbol?) {
symbol?.let { symbol?.let {
mapboxMap mapboxMap
@ -173,7 +184,12 @@ class LiveLocationMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
} }
override fun invalidate() = withState(viewModel) { viewState -> override fun invalidate() = withState(viewModel) { viewState ->
updateMap(viewState.userLocations) if (viewState.loadingMapHasFailed) {
views.mapPreviewLoadingError.isVisible = true
} else {
views.mapPreviewLoadingError.isGone = true
updateMap(viewState.userLocations)
}
updateUserListBottomSheet(viewState.userLocations) updateUserListBottomSheet(viewState.userLocations)
} }

View file

@ -61,6 +61,7 @@ class LiveLocationMapViewModel @AssistedInject constructor(
is LiveLocationMapAction.AddMapSymbol -> handleAddMapSymbol(action) is LiveLocationMapAction.AddMapSymbol -> handleAddMapSymbol(action)
is LiveLocationMapAction.RemoveMapSymbol -> handleRemoveMapSymbol(action) is LiveLocationMapAction.RemoveMapSymbol -> handleRemoveMapSymbol(action)
LiveLocationMapAction.StopSharing -> handleStopSharing() LiveLocationMapAction.StopSharing -> handleStopSharing()
LiveLocationMapAction.ShowMapLoadingError -> handleShowMapLoadingError()
} }
} }
@ -87,6 +88,10 @@ class LiveLocationMapViewModel @AssistedInject constructor(
} }
} }
private fun handleShowMapLoadingError() {
setState { copy(loadingMapHasFailed = true) }
}
override fun onLocationServiceRunning(roomIds: Set<String>) { override fun onLocationServiceRunning(roomIds: Set<String>) {
// NOOP // NOOP
} }

View file

@ -27,7 +27,8 @@ data class LiveLocationMapViewState(
/** /**
* Map to keep track of symbol ids associated to each user Id. * Map to keep track of symbol ids associated to each user Id.
*/ */
val mapSymbolIds: Map<String, Long> = emptyMap() val mapSymbolIds: Map<String, Long> = emptyMap(),
val loadingMapHasFailed: Boolean = false,
) : MavericksState { ) : MavericksState {
constructor(liveLocationMapViewArgs: LiveLocationMapViewArgs) : this( constructor(liveLocationMapViewArgs: LiveLocationMapViewArgs) : this(
roomId = liveLocationMapViewArgs.roomId roomId = liveLocationMapViewArgs.roomId

View file

@ -0,0 +1,23 @@
/*
* 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.preview
import im.vector.app.core.platform.VectorViewModelAction
sealed class LocationPreviewAction : VectorViewModelAction {
object ShowMapLoadingError : LocationPreviewAction()
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2021 New Vector Ltd * Copyright (c) 2022 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,15 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.location package im.vector.app.features.location.preview
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.mapbox.mapboxsdk.maps.MapView import com.mapbox.mapboxsdk.maps.MapView
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
@ -30,6 +33,10 @@ import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.utils.openLocation import im.vector.app.core.utils.openLocation
import im.vector.app.databinding.FragmentLocationPreviewBinding import im.vector.app.databinding.FragmentLocationPreviewBinding
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import im.vector.app.features.location.DEFAULT_PIN_ID
import im.vector.app.features.location.LocationSharingArgs
import im.vector.app.features.location.MapState
import im.vector.app.features.location.UrlMapProvider
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import javax.inject.Inject import javax.inject.Inject
@ -44,9 +51,13 @@ class LocationPreviewFragment @Inject constructor(
private val args: LocationSharingArgs by args() private val args: LocationSharingArgs by args()
private val viewModel: LocationPreviewViewModel by fragmentViewModel()
// Keep a ref to handle properly the onDestroy callback // Keep a ref to handle properly the onDestroy callback
private var mapView: WeakReference<MapView>? = null private var mapView: WeakReference<MapView>? = null
private var mapLoadingErrorListener: MapView.OnDidFailLoadingMapListener? = null
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationPreviewBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationPreviewBinding {
return FragmentLocationPreviewBinding.inflate(layoutInflater, container, false) return FragmentLocationPreviewBinding.inflate(layoutInflater, container, false)
} }
@ -55,6 +66,9 @@ class LocationPreviewFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
mapView = WeakReference(views.mapView) mapView = WeakReference(views.mapView)
mapLoadingErrorListener = MapView.OnDidFailLoadingMapListener {
viewModel.handle(LocationPreviewAction.ShowMapLoadingError)
}.also { views.mapView.addOnDidFailLoadingMapListener(it) }
views.mapView.onCreate(savedInstanceState) views.mapView.onCreate(savedInstanceState)
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
@ -63,6 +77,12 @@ class LocationPreviewFragment @Inject constructor(
} }
} }
override fun onDestroyView() {
mapLoadingErrorListener?.let { mapView?.get()?.removeOnDidFailLoadingMapListener(it) }
mapLoadingErrorListener = null
super.onDestroyView()
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
views.mapView.onResume() views.mapView.onResume()
@ -99,6 +119,10 @@ class LocationPreviewFragment @Inject constructor(
super.onDestroy() super.onDestroy()
} }
override fun invalidate() = withState(viewModel) { state ->
views.mapPreviewLoadingError.isVisible = state.loadingMapHasFailed
}
override fun getMenuRes() = R.menu.menu_location_preview override fun getMenuRes() = R.menu.menu_location_preview
override fun handleMenuItemSelected(item: MenuItem): Boolean { override fun handleMenuItemSelected(item: MenuItem): Boolean {

View file

@ -0,0 +1,48 @@
/*
* 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.preview
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.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
class LocationPreviewViewModel @AssistedInject constructor(
@Assisted private val initialState: LocationPreviewViewState,
) : VectorViewModel<LocationPreviewViewState, LocationPreviewAction, EmptyViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<LocationPreviewViewModel, LocationPreviewViewState> {
override fun create(initialState: LocationPreviewViewState): LocationPreviewViewModel
}
companion object : MavericksViewModelFactory<LocationPreviewViewModel, LocationPreviewViewState> by hiltMavericksViewModelFactory()
override fun handle(action: LocationPreviewAction) {
when (action) {
LocationPreviewAction.ShowMapLoadingError -> handleShowMapLoadingError()
}
}
private fun handleShowMapLoadingError() {
setState { copy(loadingMapHasFailed = true) }
}
}

View file

@ -0,0 +1,23 @@
/*
* 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.preview
import com.airbnb.mvrx.MavericksState
data class LocationPreviewViewState(
val loadingMapHasFailed: Boolean = false
) : MavericksState

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="70dp"
android:height="70dp"
android:viewportWidth="70"
android:viewportHeight="70">
<path
android:pathData="M34.9997,5.8335C23.7122,5.8335 14.583,15.2112 14.583,26.8059C14.583,39.2995 27.4747,56.5269 32.783,63.0882C33.9497,64.5264 36.0788,64.5264 37.2455,63.0882C42.5247,56.5269 55.4163,39.2995 55.4163,26.8059C55.4163,15.2112 46.2872,5.8335 34.9997,5.8335ZM34.9997,34.2961C30.9747,34.2961 27.708,30.9405 27.708,26.8059C27.708,22.6714 30.9747,19.3158 34.9997,19.3158C39.0247,19.3158 42.2913,22.6714 42.2913,26.8059C42.2913,30.9405 39.0247,34.2961 34.9997,34.2961Z"
android:fillColor="#C1C6CD"/>
</vector>

View file

@ -17,6 +17,13 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<im.vector.app.features.location.MapLoadingErrorView
android:id="@+id/mapPreviewLoadingError"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="180dp"
android:visibility="gone" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottomSheet" android:id="@+id/bottomSheet"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -6,9 +6,23 @@
<im.vector.app.features.location.MapTilerMapView <im.vector.app.features.location.MapTilerMapView
android:id="@+id/mapView" android:id="@+id/mapView"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:mapbox_renderTextureMode="true" app:mapbox_renderTextureMode="true"
app:showLocateButton="false" /> app:showLocateButton="false" />
<im.vector.app.features.location.MapLoadingErrorView
android:id="@+id/mapPreviewLoadingError"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -52,4 +52,14 @@
app:layout_constraintBottom_toBottomOf="@id/shareLocationOptionsPicker" app:layout_constraintBottom_toBottomOf="@id/shareLocationOptionsPicker"
app:layout_constraintEnd_toEndOf="@id/shareLocationOptionsPicker" /> app:layout_constraintEnd_toEndOf="@id/shareLocationOptionsPicker" />
<im.vector.app.features.location.MapLoadingErrorView
android:id="@+id/shareLocationMapLoadingError"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/shareLocationOptionsPicker"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -29,21 +29,16 @@
app:layout_constraintTop_toTopOf="@id/staticMapImageView" app:layout_constraintTop_toTopOf="@id/staticMapImageView"
app:layout_constraintVertical_bias="1.0" /> app:layout_constraintVertical_bias="1.0" />
<TextView <im.vector.app.features.location.MapLoadingErrorView
android:id="@+id/staticMapErrorTextView" android:id="@+id/staticMapLoadingError"
style="@style/Widget.Vector.TextView.Subtitle" android:layout_width="0dp"
android:layout_width="wrap_content" android:layout_height="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginTop="16dp"
android:layout_marginBottom="54dp"
android:text="@string/location_timeline_failed_to_load_map"
android:textColor="?vctr_content_tertiary"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/staticMapPinImageView" app:layout_constraintBottom_toTopOf="@id/liveLocationRunningBanner"
app:layout_constraintStart_toStartOf="@id/staticMapPinImageView" app:layout_constraintEnd_toEndOf="@id/staticMapImageView"
app:layout_constraintTop_toBottomOf="@id/staticMapPinImageView" app:layout_constraintStart_toStartOf="@id/staticMapImageView"
tools:visibility="visible" /> app:layout_constraintTop_toTopOf="@id/staticMapImageView"
app:mapErrorDescription="@string/location_timeline_failed_to_load_map" />
<im.vector.app.features.location.live.LiveLocationRunningBannerView <im.vector.app.features.location.live.LiveLocationRunningBannerView
android:id="@+id/liveLocationRunningBanner" android:id="@+id/liveLocationRunningBanner"

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<ImageView
android:id="@+id/mapLoadingErrorBackground"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="?vctr_system"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/mapLoadingErrorContainer"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:constraint_referenced_ids="mapLoadingErrorIcon,mapLoadingErrorDescription"
app:flow_verticalGap="12dp"
app:flow_verticalStyle="packed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/mapLoadingErrorIcon"
android:layout_width="28dp"
android:layout_height="28dp"
android:src="@drawable/ic_warning_badge"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/mapLoadingErrorDescription"
style="@style/TextAppearance.Vector.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/location_share_loading_map_error"
android:textColor="?vctr_content_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_percent="0.8" />
</merge>

View file

@ -3114,6 +3114,7 @@
<string name="location_not_available_dialog_content">${app_name} could not access your location. Please try again later.</string> <string name="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="location_share_external">Open with</string>
<string name="location_timeline_failed_to_load_map">Failed to load map</string> <string name="location_timeline_failed_to_load_map">Failed to load map</string>
<string name="location_share_loading_map_error">Unable to load map\nThis home server may not be configured to display maps.</string>
<string name="location_share_live_enabled">Live location enabled</string> <string name="location_share_live_enabled">Live location enabled</string>
<string name="location_share_live_started">Loading live location…</string> <string name="location_share_live_started">Loading live location…</string>
<string name="location_share_live_ended">Live location ended</string> <string name="location_share_live_ended">Live location ended</string>