Handling map loading error in sharing and preview fragment

This commit is contained in:
Maxime NATUREL 2022-08-01 09:18:55 +02:00
parent 87ca9606b3
commit e0e06c6ac8
10 changed files with 168 additions and 23 deletions

View File

@ -54,6 +54,7 @@ import im.vector.app.features.home.room.list.RoomListViewModel
import im.vector.app.features.home.room.list.home.HomeRoomListViewModel import im.vector.app.features.home.room.list.home.HomeRoomListViewModel
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel 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.LocationPreviewViewModel
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.login.LoginViewModel import im.vector.app.features.login.LoginViewModel
@ -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

@ -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
import im.vector.app.core.platform.VectorViewModelAction
sealed class LocationPreviewAction : VectorViewModelAction {
object ShowMapLoadingError : LocationPreviewAction()
}

View File

@ -21,8 +21,11 @@ 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
@ -44,9 +47,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 +62,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 +73,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 +115,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
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
import com.airbnb.mvrx.MavericksState
data class LocationPreviewViewState(
val loadingMapHasFailed: Boolean = false
) : MavericksState

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

@ -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

@ -35,16 +35,6 @@
app:layout_constraintStart_toStartOf="@id/mapView" app:layout_constraintStart_toStartOf="@id/mapView"
app:layout_constraintTop_toTopOf="@id/mapView" /> app:layout_constraintTop_toTopOf="@id/mapView" />
<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" />
<im.vector.app.features.location.option.LocationSharingOptionPickerView <im.vector.app.features.location.option.LocationSharingOptionPickerView
android:id="@+id/shareLocationOptionsPicker" android:id="@+id/shareLocationOptionsPicker"
android:layout_width="0dp" android:layout_width="0dp"
@ -62,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>