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