Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/5536.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Live location sharing: adding build config field and show permission dialog
2 changes: 2 additions & 0 deletions vector/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,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
}
Expand All @@ -238,6 +239,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
Expand Down
1 change: 1 addition & 0 deletions vector/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<!-- Location Sharing -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

<!-- Jitsi SDK is now API23+ -->
<uses-sdk tools:overrideLibrary="org.jitsi.meet.sdk,com.oney.WebRTCModule,com.learnium.RNDeviceInfo,com.reactnativecommunity.asyncstorage,com.ocetnik.timer,com.calendarevents,com.reactnativecommunity.netinfo,com.kevinresol.react_native_default_preference,com.rnimmersive,com.corbt.keepawake,com.BV.LinearGradient,com.horcrux.svg,com.oblador.performance,com.reactnativecommunity.slider,com.brentvatne.react" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,6 +33,7 @@ import im.vector.app.R
import im.vector.app.core.platform.VectorBaseActivity

// Permissions sets
val PERMISSIONS_EMPTY = emptyList<String>()
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)
Expand All @@ -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<String>()
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
Expand Down Expand Up @@ -123,6 +128,7 @@ fun checkPermissions(permissionsToBeGranted: List<String>,
.setPositiveButton(R.string.ok) { _, _ ->
activityResultLauncher.launch(missingPermissions.toTypedArray())
}
.setNegativeButton(R.string.action_not_now, null)
.show()
} else {
// some permissions are not granted, ask permissions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<MapView>? = null

Expand Down Expand Up @@ -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
}
Expand All @@ -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() {
Expand Down Expand Up @@ -137,8 +149,20 @@ 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()
}
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we could add a .setNegativeButton(R.string.action_not_now, null) inside the implementation of the checkPermissions method for the dialog which is displayed when a message should be displayed? Right now the user is forced to go to the permission settings screen and there is no way to disable the dialog.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice idea, done.

)) {
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<LocationSharingOption> = 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class LocationSharingOptionPickerView @JvmOverloads constructor(
applyBackground()
}

fun render(vararg options: LocationSharingOption) {
fun render(options: Set<LocationSharingOption> = emptySet()) {
val optionsNumber = options.toSet().size
val isPinnedVisible = options.contains(LocationSharingOption.PINNED)
val isUserCurrentVisible = options.contains(LocationSharingOption.USER_CURRENT)
Expand Down
2 changes: 2 additions & 0 deletions vector/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2937,6 +2937,8 @@
<string name="a11y_location_share_option_user_live_icon">Share live location</string>
<string name="location_share_option_pinned">Share this location</string>
<string name="a11y_location_share_option_pinned_icon">Share this location</string>
<string name="location_in_background_missing_permission_dialog_title">Allow access</string>
<string name="location_in_background_missing_permission_dialog_content">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.</string>
<string name="location_not_available_dialog_title">${app_name} could not access your location</string>
<string name="location_not_available_dialog_content">${app_name} could not access your location. Please try again later.</string>
<string name="location_share_external">Open with</string>
Expand Down