diff --git a/changelog.d/6711.feature b/changelog.d/6711.feature new file mode 100644 index 0000000000..cff718affd --- /dev/null +++ b/changelog.d/6711.feature @@ -0,0 +1 @@ +[Location Share] Render fallback UI when map fails to load diff --git a/library/ui-styles/src/main/res/values/stylable_map_loading_error_view.xml b/library/ui-styles/src/main/res/values/stylable_map_loading_error_view.xml new file mode 100644 index 0000000000..911167e52a --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_map_loading_error_view.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 3cdc8a1afe..e86b350534 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -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.home.HomeRoomListFragment 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.preview.LocationPreviewFragment import im.vector.app.features.login.LoginCaptchaFragment import im.vector.app.features.login.LoginFragment import im.vector.app.features.login.LoginGenericTextInputFormFragment diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 9b511a1bfd..331b4afa18 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -56,6 +56,7 @@ 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.location.live.map.LiveLocationMapViewModel +import im.vector.app.features.location.preview.LocationPreviewViewModel import im.vector.app.features.login.LoginViewModel import im.vector.app.features.login2.LoginViewModel2 import im.vector.app.features.login2.created.AccountCreatedViewModel @@ -605,6 +606,11 @@ interface MavericksViewModelModule { @MavericksViewModelKey(LocationSharingViewModel::class) fun createLocationSharingViewModelFactory(factory: LocationSharingViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds + @IntoMap + @MavericksViewModelKey(LocationPreviewViewModel::class) + fun createLocationPreviewViewModelFactory(factory: LocationPreviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(VectorAttachmentViewerViewModel::class) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt index b7790b8a30..4903b8c8cf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt @@ -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.style.TimelineMessageLayout 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( @LayoutRes layoutId: Int = R.layout.item_timeline_event_base @@ -86,8 +88,10 @@ abstract class AbsMessageLocationItem( target: Target?, isFirstResource: Boolean ): Boolean { - holder.staticMapPinImageView.setImageResource(R.drawable.ic_location_pin_failed) - holder.staticMapErrorTextView.isVisible = true + holder.staticMapPinImageView.setImageDrawable(null) + holder.staticMapLoadingErrorView.isVisible = true + val mapErrorViewState = MapLoadingErrorViewState(imageCornerTransformation) + holder.staticMapLoadingErrorView.render(mapErrorViewState) holder.staticMapCopyrightTextView.isVisible = false return false } @@ -103,7 +107,7 @@ abstract class AbsMessageLocationItem( // we are not using Glide since it does not display it correctly when there is no user photo holder.staticMapPinImageView.setImageDrawable(pinDrawable) } - holder.staticMapErrorTextView.isVisible = false + holder.staticMapLoadingErrorView.isVisible = false holder.staticMapCopyrightTextView.isVisible = true return false } @@ -115,7 +119,7 @@ abstract class AbsMessageLocationItem( abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) { val staticMapImageView by bind(R.id.staticMapImageView) val staticMapPinImageView by bind(R.id.staticMapPinImageView) - val staticMapErrorTextView by bind(R.id.staticMapErrorTextView) + val staticMapLoadingErrorView by bind(R.id.staticMapLoadingError) val staticMapCopyrightTextView by bind(R.id.staticMapCopyrightTextView) } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt index 264b1f0e0b..5a1edbf8a1 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt @@ -25,4 +25,5 @@ sealed class LocationSharingAction : VectorViewModelAction { object ZoomToUserLocation : LocationSharingAction() object LiveLocationSharingRequested : LocationSharingAction() data class StartLiveLocationSharing(val durationMillis: Long) : LocationSharingAction() + object ShowMapLoadingError : LocationSharingAction() } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt index 169af4a5a2..9eddcad649 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt @@ -23,6 +23,7 @@ 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 im.vector.app.features.location.preview.LocationPreviewFragment import kotlinx.parcelize.Parcelize @Parcelize diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index ca524a0126..d96410010e 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -24,6 +24,7 @@ import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.fragment.app.setFragmentResultListener import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.fragmentViewModel @@ -69,6 +70,7 @@ class LocationSharingFragment @Inject constructor( private var mapView: WeakReference? = null private var hasRenderedUserAvatar = false + private var mapLoadingErrorListener: MapView.OnDidFailLoadingMapListener? = null override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationSharingBinding { return FragmentLocationSharingBinding.inflate(inflater, container, false) @@ -87,6 +89,9 @@ class LocationSharingFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) mapView = WeakReference(views.mapView) + mapLoadingErrorListener = MapView.OnDidFailLoadingMapListener { + viewModel.handle(LocationSharingAction.ShowMapLoadingError) + }.also { views.mapView.addOnDidFailLoadingMapListener(it) } views.mapView.onCreate(savedInstanceState) 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() { super.onResume() views.mapView.onResume() @@ -256,20 +267,27 @@ class LocationSharingFragment @Inject constructor( } private fun updateMap(state: LocationSharingViewState) { - // first, update the options view - val options: Set = when (state.areTargetAndUserLocationEqual) { - true -> setOf(LocationSharingOption.USER_CURRENT, LocationSharingOption.USER_LIVE) - false -> setOf(LocationSharingOption.PINNED) - else -> emptySet() - } - views.shareLocationOptionsPicker.render(options) + if (state.loadingMapHasFailed) { + views.shareLocationOptionsPicker.render(emptySet()) + views.shareLocationMapLoadingError.isVisible = true + } else { + // first, update the options view + val options: Set = when (state.areTargetAndUserLocationEqual) { + 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 - views.shareLocationOptionsPicker.post { - val mapState = state - .toMapState() - .copy(logoMarginBottom = views.shareLocationOptionsPicker.height) - views.mapView.render(mapState) + // then, update the map using the height of the options view after it has been rendered + views.shareLocationOptionsPicker.post { + val mapState = state + .toMapState() + .copy(logoMarginBottom = views.shareLocationOptionsPicker.height) + views.mapView.render(mapState) + } + + views.shareLocationMapLoadingError.isGone = true } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt index 8056b72d57..28e37a38eb 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt @@ -152,6 +152,7 @@ class LocationSharingViewModel @AssistedInject constructor( LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction() LocationSharingAction.LiveLocationSharingRequested -> handleLiveLocationSharingRequestedAction() 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) { Timber.d("onLocationUpdate()") setState { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt index d5226eacfb..c7a2349afa 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt @@ -36,6 +36,7 @@ data class LocationSharingViewState( val lastKnownUserLocation: LocationData? = null, val locationTargetDrawable: Drawable? = null, val canShareLiveLocation: Boolean = false, + val loadingMapHasFailed: Boolean = false ) : MavericksState { constructor(locationSharingArgs: LocationSharingArgs) : this( diff --git a/vector/src/main/java/im/vector/app/features/location/MapLoadingErrorView.kt b/vector/src/main/java/im/vector/app/features/location/MapLoadingErrorView.kt new file mode 100644 index 0000000000..8ea4fcac54 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/MapLoadingErrorView.kt @@ -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) + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/MapLoadingErrorViewState.kt b/vector/src/main/java/im/vector/app/features/location/MapLoadingErrorViewState.kt new file mode 100644 index 0000000000..8098d26a12 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/MapLoadingErrorViewState.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation + +data class MapLoadingErrorViewState(val backgroundTransformation: BitmapTransformation) diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapAction.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapAction.kt index 2a07934146..295d6b5d41 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapAction.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapAction.kt @@ -22,4 +22,5 @@ sealed class LiveLocationMapAction : VectorViewModelAction { data class AddMapSymbol(val key: String, val value: Long) : LiveLocationMapAction() data class RemoveMapSymbol(val key: String) : LiveLocationMapAction() object StopSharing : LiveLocationMapAction() + object ShowMapLoadingError : LiveLocationMapAction() } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewFragment.kt index 283774dbc6..85095e7c9f 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewFragment.kt @@ -71,11 +71,13 @@ class LiveLocationMapViewFragment @Inject constructor() : VectorBaseFragment? = null + private var mapView: MapView? = null private var symbolManager: SymbolManager? = null private var mapStyle: Style? = null private val pendingLiveLocations = mutableListOf() private var isMapFirstUpdate = true private var onSymbolClickListener: OnSymbolClickListener? = null + private var mapLoadingErrorListener: MapView.OnDidFailLoadingMapListener? = null override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLiveLocationMapViewBinding { return FragmentLiveLocationMapViewBinding.inflate(layoutInflater, container, false) @@ -84,6 +86,7 @@ class LiveLocationMapViewFragment @Inject constructor() : VectorBaseFragment + (mapFragment.view as? MapView)?.let { + mapView = it + listenMapLoadingError(it) + } lifecycleScope.launch { mapboxMap.setStyle(urlMapProvider.getMapUrl()) { style -> mapStyle = style @@ -141,6 +146,12 @@ class LiveLocationMapViewFragment @Inject constructor() : VectorBaseFragment - updateMap(viewState.userLocations) + if (viewState.loadingMapHasFailed) { + views.mapPreviewLoadingError.isVisible = true + } else { + views.mapPreviewLoadingError.isGone = true + updateMap(viewState.userLocations) + } updateUserListBottomSheet(viewState.userLocations) } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewModel.kt index 280789e4c6..33c584ff85 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewModel.kt @@ -61,6 +61,7 @@ class LiveLocationMapViewModel @AssistedInject constructor( is LiveLocationMapAction.AddMapSymbol -> handleAddMapSymbol(action) is LiveLocationMapAction.RemoveMapSymbol -> handleRemoveMapSymbol(action) 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) { // NOOP } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewState.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewState.kt index 615b4381e2..ddd1cd2369 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewState.kt @@ -27,7 +27,8 @@ data class LiveLocationMapViewState( /** * Map to keep track of symbol ids associated to each user Id. */ - val mapSymbolIds: Map = emptyMap() + val mapSymbolIds: Map = emptyMap(), + val loadingMapHasFailed: Boolean = false, ) : MavericksState { constructor(liveLocationMapViewArgs: LiveLocationMapViewArgs) : this( roomId = liveLocationMapViewArgs.roomId diff --git a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewAction.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewAction.kt new file mode 100644 index 0000000000..38f6952f67 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewAction.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewFragment.kt similarity index 78% rename from vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt rename to vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewFragment.kt index 131119a7aa..8285d0156b 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewFragment.kt @@ -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"); * you may not use this file except in compliance with the License. @@ -14,15 +14,18 @@ * limitations under the License. */ -package im.vector.app.features.location +package im.vector.app.features.location.preview import android.os.Bundle import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import com.mapbox.mapboxsdk.maps.MapView import im.vector.app.R 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.databinding.FragmentLocationPreviewBinding 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 javax.inject.Inject @@ -44,9 +51,13 @@ class LocationPreviewFragment @Inject constructor( private val args: LocationSharingArgs by args() + private val viewModel: LocationPreviewViewModel by fragmentViewModel() + // Keep a ref to handle properly the onDestroy callback private var mapView: WeakReference? = null + private var mapLoadingErrorListener: MapView.OnDidFailLoadingMapListener? = null + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationPreviewBinding { return FragmentLocationPreviewBinding.inflate(layoutInflater, container, false) } @@ -55,6 +66,9 @@ class LocationPreviewFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) mapView = WeakReference(views.mapView) + mapLoadingErrorListener = MapView.OnDidFailLoadingMapListener { + viewModel.handle(LocationPreviewAction.ShowMapLoadingError) + }.also { views.mapView.addOnDidFailLoadingMapListener(it) } views.mapView.onCreate(savedInstanceState) 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() { super.onResume() views.mapView.onResume() @@ -99,6 +119,10 @@ class LocationPreviewFragment @Inject constructor( super.onDestroy() } + override fun invalidate() = withState(viewModel) { state -> + views.mapPreviewLoadingError.isVisible = state.loadingMapHasFailed + } + override fun getMenuRes() = R.menu.menu_location_preview override fun handleMenuItemSelected(item: MenuItem): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt new file mode 100644 index 0000000000..f0698249ce --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt @@ -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(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: LocationPreviewViewState): LocationPreviewViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + override fun handle(action: LocationPreviewAction) { + when (action) { + LocationPreviewAction.ShowMapLoadingError -> handleShowMapLoadingError() + } + } + + private fun handleShowMapLoadingError() { + setState { copy(loadingMapHasFailed = true) } + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewState.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewState.kt new file mode 100644 index 0000000000..96e8316323 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewState.kt @@ -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 diff --git a/vector/src/main/res/drawable/ic_location_pin_failed.xml b/vector/src/main/res/drawable/ic_location_pin_failed.xml deleted file mode 100644 index 250d048836..0000000000 --- a/vector/src/main/res/drawable/ic_location_pin_failed.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/vector/src/main/res/layout/fragment_live_location_map_view.xml b/vector/src/main/res/layout/fragment_live_location_map_view.xml index 81db466dab..369404ddbd 100644 --- a/vector/src/main/res/layout/fragment_live_location_map_view.xml +++ b/vector/src/main/res/layout/fragment_live_location_map_view.xml @@ -17,6 +17,13 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + + + diff --git a/vector/src/main/res/layout/fragment_location_sharing.xml b/vector/src/main/res/layout/fragment_location_sharing.xml index cd15f418ea..4f0825b992 100644 --- a/vector/src/main/res/layout/fragment_location_sharing.xml +++ b/vector/src/main/res/layout/fragment_location_sharing.xml @@ -52,4 +52,14 @@ app:layout_constraintBottom_toBottomOf="@id/shareLocationOptionsPicker" app:layout_constraintEnd_toEndOf="@id/shareLocationOptionsPicker" /> + + diff --git a/vector/src/main/res/layout/item_timeline_event_location_stub.xml b/vector/src/main/res/layout/item_timeline_event_location_stub.xml index e4c87dab71..8de98b2260 100644 --- a/vector/src/main/res/layout/item_timeline_event_location_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_location_stub.xml @@ -29,21 +29,16 @@ app:layout_constraintTop_toTopOf="@id/staticMapImageView" app:layout_constraintVertical_bias="1.0" /> - + app:layout_constraintBottom_toTopOf="@id/liveLocationRunningBanner" + app:layout_constraintEnd_toEndOf="@id/staticMapImageView" + app:layout_constraintStart_toStartOf="@id/staticMapImageView" + app:layout_constraintTop_toTopOf="@id/staticMapImageView" + app:mapErrorDescription="@string/location_timeline_failed_to_load_map" /> + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 6b4eec1c9c..d1b2d237d9 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3114,6 +3114,7 @@ ${app_name} could not access your location. Please try again later. Open with Failed to load map + Unable to load map\nThis home server may not be configured to display maps. Live location enabled Loading live location… Live location ended