diff --git a/changelog.d/5536.feature b/changelog.d/5536.feature new file mode 100644 index 0000000000..bd0160f2fe --- /dev/null +++ b/changelog.d/5536.feature @@ -0,0 +1 @@ +Live location sharing: adding build config field and show permission dialog diff --git a/vector/build.gradle b/vector/build.gradle index 7a517f62c8..aeaad19e02 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -230,6 +230,7 @@ android { buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" // Set to true if you want to enable strict mode in debug buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false" + buildConfigField "Boolean", "ENABLE_LIVE_LOCATION_SHARING", "true" signingConfig signingConfigs.debug } @@ -239,6 +240,7 @@ android { buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false" + buildConfigField "Boolean", "ENABLE_LIVE_LOCATION_SHARING", "false" postprocessing { removeUnusedCode true diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 58b1bc177c..1d99fba91a 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -45,6 +45,7 @@ + diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt index dabf11b9d3..eada3a4f25 100644 --- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt @@ -19,6 +19,7 @@ package im.vector.app.core.utils import android.Manifest import android.app.Activity import android.content.pm.PackageManager +import android.os.Build import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -32,6 +33,7 @@ import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity // Permissions sets +val PERMISSIONS_EMPTY = emptyList() val PERMISSIONS_FOR_AUDIO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO) val PERMISSIONS_FOR_VIDEO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) val PERMISSIONS_FOR_VOICE_MESSAGE = listOf(Manifest.permission.RECORD_AUDIO) @@ -40,9 +42,12 @@ val PERMISSIONS_FOR_MEMBERS_SEARCH = listOf(Manifest.permission.READ_CONTACTS) val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA) val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS) -val PERMISSIONS_FOR_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) - -val PERMISSIONS_EMPTY = emptyList() +val PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) +val PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION) +} else { + PERMISSIONS_EMPTY +} // This is not ideal to store the value like that, but it works private var permissionDialogDisplayed = false @@ -123,6 +128,7 @@ fun checkPermissions(permissionsToBeGranted: List, .setPositiveButton(R.string.ok) { _, _ -> activityResultLauncher.launch(missingPermissions.toTypedArray()) } + .setNegativeButton(R.string.action_not_now, null) .show() } else { // some permissions are not granted, ask permissions diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt index a15bd52174..7fcbb6bae6 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt @@ -37,7 +37,7 @@ import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.core.utils.PERMISSIONS_EMPTY -import im.vector.app.core.utils.PERMISSIONS_FOR_LOCATION_SHARING +import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding @@ -215,6 +215,6 @@ class AttachmentTypeSelectorView(context: Context, STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker), CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact), POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll), - LOCATION(PERMISSIONS_FOR_LOCATION_SHARING, R.string.tooltip_attachment_location) + LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, R.string.tooltip_attachment_location) } } diff --git a/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt b/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt new file mode 100644 index 0000000000..8f424af9ec --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/DefaultLocationSharingNavigator.kt @@ -0,0 +1,36 @@ +/* + * 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.app.Activity +import im.vector.app.core.utils.openAppSettingsPage + +class DefaultLocationSharingNavigator constructor(val activity: Activity?) : LocationSharingNavigator { + + override var goingToAppSettings: Boolean = false + + override fun quit() { + activity?.finish() + } + + override fun goToAppSettings() { + activity?.let { + goingToAppSettings = true + openAppSettingsPage(it) + } + } +} 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 ec47c23ea7..d7d686ee60 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 @@ -23,4 +23,5 @@ sealed class LocationSharingAction : VectorViewModelAction { data class PinnedLocationSharing(val locationData: LocationData?) : LocationSharingAction() data class LocationTargetChange(val locationData: LocationData) : LocationSharingAction() object ZoomToUserLocation : LocationSharingAction() + object StartLiveLocationSharing : LocationSharingAction() } 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 e9e96e676c..c4dccc1b73 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 @@ -27,9 +27,14 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.mapbox.mapboxsdk.maps.MapView +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING +import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentLocationSharingBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider @@ -49,6 +54,8 @@ class LocationSharingFragment @Inject constructor( private val viewModel: LocationSharingViewModel by fragmentViewModel() + private val locationSharingNavigator: LocationSharingNavigator by lazy { DefaultLocationSharingNavigator(activity) } + // Keep a ref to handle properly the onDestroy callback private var mapView: WeakReference? = null @@ -76,8 +83,8 @@ class LocationSharingFragment @Inject constructor( viewModel.observeViewEvents { when (it) { + LocationSharingViewEvents.Close -> locationSharingNavigator.quit() LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError() - LocationSharingViewEvents.Close -> activity?.finish() is LocationSharingViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it) }.exhaustive } @@ -86,6 +93,11 @@ class LocationSharingFragment @Inject constructor( override fun onResume() { super.onResume() views.mapView.onResume() + if (locationSharingNavigator.goingToAppSettings) { + locationSharingNavigator.goingToAppSettings = false + // retry to start live location + tryStartLiveLocationSharing() + } } override fun onPause() { @@ -137,12 +149,24 @@ class LocationSharingFragment @Inject constructor( .setTitle(R.string.location_not_available_dialog_title) .setMessage(R.string.location_not_available_dialog_content) .setPositiveButton(R.string.ok) { _, _ -> - activity?.finish() + locationSharingNavigator.quit() } .setCancelable(false) .show() } + private fun handleMissingBackgroundLocationPermission() { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.location_in_background_missing_permission_dialog_title) + .setMessage(R.string.location_in_background_missing_permission_dialog_content) + .setPositiveButton(R.string.settings) { _, _ -> + locationSharingNavigator.goToAppSettings() + } + .setNegativeButton(R.string.action_not_now, null) + .setCancelable(false) + .show() + } + private fun initLocateButton() { views.mapView.locateButton.setOnClickListener { viewModel.handle(LocationSharingAction.ZoomToUserLocation) @@ -164,22 +188,58 @@ class LocationSharingFragment @Inject constructor( viewModel.handle(LocationSharingAction.CurrentUserLocationSharing) } views.shareLocationOptionsPicker.optionUserLive.debouncedClicks { - // TODO + tryStartLiveLocationSharing() } } + private val foregroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted && checkPermissions(PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING, requireActivity(), backgroundLocationResultLauncher)) { + startLiveLocationSharing() + } else if (deniedPermanently) { + handleMissingBackgroundLocationPermission() + } + } + + private val backgroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + startLiveLocationSharing() + } else if (deniedPermanently) { + handleMissingBackgroundLocationPermission() + } + } + + private fun tryStartLiveLocationSharing() { + // we need to re-check foreground location to be sure it has not changed after landing on this screen + if (checkPermissions(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, requireActivity(), foregroundLocationResultLauncher) && + checkPermissions( + PERMISSIONS_FOR_BACKGROUND_LOCATION_SHARING, + requireActivity(), + backgroundLocationResultLauncher, + R.string.location_in_background_missing_permission_dialog_content + )) { + startLiveLocationSharing() + } + } + + private fun startLiveLocationSharing() { + viewModel.handle(LocationSharingAction.StartLiveLocationSharing) + } + private fun updateMap(state: LocationSharingViewState) { // first, update the options view - when (state.areTargetAndUserLocationEqual) { - // TODO activate USER_LIVE option when implemented - true -> views.shareLocationOptionsPicker.render( - LocationSharingOption.USER_CURRENT - ) - false -> views.shareLocationOptionsPicker.render( - LocationSharingOption.PINNED - ) - else -> views.shareLocationOptionsPicker.render() + val options: Set = when (state.areTargetAndUserLocationEqual) { + true -> { + if (BuildConfig.ENABLE_LIVE_LOCATION_SHARING) { + setOf(LocationSharingOption.USER_CURRENT, LocationSharingOption.USER_LIVE) + } else { + setOf(LocationSharingOption.USER_CURRENT) + } + } + 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 diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.kt new file mode 100644 index 0000000000..8927da9239 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingNavigator.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 + +interface LocationSharingNavigator { + var goingToAppSettings: Boolean + fun quit() + fun goToAppSettings() +} 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 25bc482412..639666e63f 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 @@ -38,6 +38,7 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.util.toMatrixItem +import timber.log.Timber /** * Sampling period to compare target location and user location. @@ -120,6 +121,7 @@ class LocationSharingViewModel @AssistedInject constructor( is LocationSharingAction.PinnedLocationSharing -> handlePinnedLocationSharingAction(action) is LocationSharingAction.LocationTargetChange -> handleLocationTargetChangeAction(action) LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction() + LocationSharingAction.StartLiveLocationSharing -> handleStartLiveLocationSharingAction() }.exhaustive } @@ -157,6 +159,11 @@ class LocationSharingViewModel @AssistedInject constructor( } } + private fun handleStartLiveLocationSharingAction() { + // TODO start sharing live location and update view state + Timber.d("live location sharing started") + } + override fun onLocationUpdate(locationData: LocationData) { setState { copy(lastKnownUserLocation = locationData) diff --git a/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionPickerView.kt b/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionPickerView.kt index 1aea1ff613..8a603a1a56 100644 --- a/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionPickerView.kt +++ b/vector/src/main/java/im/vector/app/features/location/option/LocationSharingOptionPickerView.kt @@ -58,7 +58,7 @@ class LocationSharingOptionPickerView @JvmOverloads constructor( applyBackground() } - fun render(vararg options: LocationSharingOption) { + fun render(options: Set = emptySet()) { val optionsNumber = options.toSet().size val isPinnedVisible = options.contains(LocationSharingOption.PINNED) val isUserCurrentVisible = options.contains(LocationSharingOption.USER_CURRENT) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 162ab3e119..428be3209f 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2937,6 +2937,8 @@ Share live location Share this location Share this location + Allow access + If you’d like to share your Live location, ${app_name} needs location access all the time when the app is in the background.\nWe will only access your location for the duration that you choose. ${app_name} could not access your location ${app_name} could not access your location. Please try again later. Open with