diff --git a/vector/src/main/java/im/vector/app/features/location/LocationDialog.kt b/vector/src/main/java/im/vector/app/features/location/LocationDialog.kt new file mode 100644 index 0000000000..81ce75e57d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationDialog.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R + +fun Fragment.showUserLocationNotAvailableErrorDialog(onConfirmListener: () -> Unit) { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.location_not_available_dialog_title) + .setMessage(R.string.location_not_available_dialog_content) + .setPositiveButton(R.string.ok) { _, _ -> + onConfirmListener() + } + .setCancelable(false) + .show() +} 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 779818b3d6..0fdf9d04cd 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 @@ -176,14 +176,7 @@ class LocationSharingFragment : } private fun handleLocationNotAvailableError() { - MaterialAlertDialogBuilder(requireActivity()) - .setTitle(R.string.location_not_available_dialog_title) - .setMessage(R.string.location_not_available_dialog_content) - .setPositiveButton(R.string.ok) { _, _ -> - locationSharingNavigator.quit() - } - .setCancelable(false) - .show() + showUserLocationNotAvailableErrorDialog { locationSharingNavigator.quit() } } private fun handleLiveLocationSharingNotEnoughPermission() { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index c617277f3f..7e47600715 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -90,6 +90,7 @@ class LocationTracker @Inject constructor( @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) fun start() { + // TODO start only if not already started Timber.d("start()") if (locationManager == null) { 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 index 38f6952f67..094c2206fa 100644 --- 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 @@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class LocationPreviewAction : VectorViewModelAction { object ShowMapLoadingError : LocationPreviewAction() + object ZoomToUserLocation : LocationPreviewAction() } diff --git a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewFragment.kt index 082cee02f0..8be3b063b0 100644 --- a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewFragment.kt @@ -31,13 +31,18 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider +import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.openLocation +import im.vector.app.core.utils.registerForPermissionsResult 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 im.vector.app.features.location.showUserLocationNotAvailableErrorDialog import java.lang.ref.WeakReference import javax.inject.Inject @@ -78,6 +83,28 @@ class LocationPreviewFragment : views.mapView.initialize(urlMapProvider.getMapUrl()) loadPinDrawable() } + + observeViewEvents() + initLocateButton() + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + LocationPreviewViewEvents.UserLocationNotAvailableError -> handleUserLocationNotAvailableError() + is LocationPreviewViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it) + } + } + } + + private fun handleUserLocationNotAvailableError() { + showUserLocationNotAvailableErrorDialog { + // do nothing + } + } + + private fun handleZoomToUserLocationEvent(event: LocationPreviewViewEvents.ZoomToUserLocation) { + views.mapView.zoomToLocation(event.userLocation) } override fun onDestroyView() { @@ -124,6 +151,12 @@ class LocationPreviewFragment : override fun invalidate() = withState(viewModel) { state -> views.mapPreviewLoadingError.isVisible = state.loadingMapHasFailed + // TODO render pin for user location + if(state.isLoadingUserLocation) { + showLoadingDialog() + } else { + dismissLoadingDialog() + } } override fun getMenuRes() = R.menu.menu_location_preview @@ -154,10 +187,30 @@ class LocationPreviewFragment : zoomOnlyOnce = true, userLocationData = location, pinId = args.locationOwnerId ?: DEFAULT_PIN_ID, - pinDrawable = pinDrawable + pinDrawable = pinDrawable, ) ) } } } + + private fun initLocateButton() { + views.mapView.locateButton.setOnClickListener { + if (checkPermissions(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, requireActivity(), foregroundLocationResultLauncher)) { + zoomToUserLocation() + } + } + } + + private fun zoomToUserLocation() { + viewModel.handle(LocationPreviewAction.ZoomToUserLocation) + } + + private val foregroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + zoomToUserLocation() + } else if (deniedPermanently) { + activity?.onPermissionDeniedDialog(R.string.denied_permission_generic) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewEvents.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewEvents.kt new file mode 100644 index 0000000000..605c240d06 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewEvents.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location.preview + +import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.features.location.LocationData + +sealed class LocationPreviewViewEvents : VectorViewEvents { + data class ZoomToUserLocation(val userLocation: LocationData) : LocationPreviewViewEvents() + object UserLocationNotAvailableError : LocationPreviewViewEvents() +} 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 index f0698249ce..3b4c3cf498 100644 --- 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 @@ -22,12 +22,18 @@ 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 +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.LocationTracker +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch class LocationPreviewViewModel @AssistedInject constructor( @Assisted private val initialState: LocationPreviewViewState, -) : VectorViewModel(initialState) { + private val locationTracker: LocationTracker, +) : VectorViewModel(initialState), LocationTracker.Callback { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -36,13 +42,61 @@ class LocationPreviewViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + init { + initLocationTracking() + } + + private fun initLocationTracking() { + locationTracker.addCallback(this) + locationTracker.locations + .onEach(::onLocationUpdate) + .launchIn(viewModelScope) + } + + override fun onCleared() { + super.onCleared() + locationTracker.removeCallback(this) + } + override fun handle(action: LocationPreviewAction) { when (action) { LocationPreviewAction.ShowMapLoadingError -> handleShowMapLoadingError() + LocationPreviewAction.ZoomToUserLocation -> handleZoomToUserLocationAction() } } private fun handleShowMapLoadingError() { setState { copy(loadingMapHasFailed = true) } } + + private fun handleZoomToUserLocationAction() = withState { state -> + if (!state.isLoadingUserLocation) { + setState { + copy(isLoadingUserLocation = true) + } + viewModelScope.launch(Dispatchers.Main) { + locationTracker.start() + locationTracker.requestLastKnownLocation() + } + } + } + + override fun onNoLocationProviderAvailable() { + _viewEvents.post(LocationPreviewViewEvents.UserLocationNotAvailableError) + } + + private fun onLocationUpdate(locationData: LocationData) { + withState { state -> + if (state.isLoadingUserLocation) { + _viewEvents.post(LocationPreviewViewEvents.ZoomToUserLocation(locationData)) + } + } + + setState { + copy( + lastKnownUserLocation = locationData, + isLoadingUserLocation = false, + ) + } + } } 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 index 96e8316323..db37ddb420 100644 --- 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 @@ -17,7 +17,10 @@ package im.vector.app.features.location.preview import com.airbnb.mvrx.MavericksState +import im.vector.app.features.location.LocationData data class LocationPreviewViewState( - val loadingMapHasFailed: Boolean = false + val loadingMapHasFailed: Boolean = false, + val isLoadingUserLocation: Boolean = false, + val lastKnownUserLocation: LocationData? = null, ) : MavericksState diff --git a/vector/src/main/res/layout/fragment_location_preview.xml b/vector/src/main/res/layout/fragment_location_preview.xml index 126edbb5e1..e70baa1581 100644 --- a/vector/src/main/res/layout/fragment_location_preview.xml +++ b/vector/src/main/res/layout/fragment_location_preview.xml @@ -13,7 +13,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:mapbox_renderTextureMode="true" - app:showLocateButton="false" /> + app:showLocateButton="true" />