-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WorldScaleSceneView: Handle permissions and implement world tracking #709
WorldScaleSceneView: Handle permissions and implement world tracking #709
Conversation
@@ -39,7 +39,6 @@ fun MainScreen() { | |||
} | |||
WorldScaleSceneView( | |||
arcGISScene = arcGISScene, | |||
locationDataSource = SystemLocationDataSource(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will no longer be passed in by the user
launch { | ||
locationDataSource.status.collect { | ||
var isLocationDataSourceStarted by remember { mutableStateOf(false) } | ||
if (hasCompletedPermissionRequest && context.checkPermissionsGranted()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
context.checkPermissionsGranted()
will re-check the permissions with the system every time it is called, rather than storing that in a mutable state. I found this was much more predictable and easy to understand than using state, and it should only run once when hasCompletedPermissionRequest
is true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When hasCompletedPermissionRequest
is true, doesn't this mean that all required permissions have been granted? Why do we need to then check again with checkPermissionsGranted()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the version you reviewed, hasCompletedPermissionRequest
could be true even if all permissions were not granted. However given your question I thought about it more and reworked it so that the mutable state encodes whether all permissions were granted.
} | ||
|
||
Box(modifier = modifier) { | ||
if (cameraPermissionGranted && arCoreInstalled) { | ||
if (isLocationDataSourceStarted && arCoreInstalled) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isLocationDataSourceStarted
will only be true if permissions have already been granted
hasCompletedRequest.value = true | ||
} | ||
|
||
if (!hasLaunchedRequest) { | ||
SideEffect { | ||
hasLaunchedRequest = true | ||
launcher.launch(permissionsToRequest.toTypedArray()) | ||
} | ||
} | ||
|
||
return hasCompletedRequest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that hasLaunchedRequest
is used internally to this function to ensure the request is only launched once. hasCompletedRequest
is used for the WorldScaleSceneView to know it's okay to continue with the startup sequence
…hub.com/Esri/arcgis-maps-sdk-kotlin-toolkit into hud10837/implement-world-tracking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing obviously wrong to me, only a few queries & suggestions
toolkit/ar/src/androidTest/java/com/arcgismaps/toolkit/ar/LocationFilteringTests.kt
Outdated
Show resolved
Hide resolved
toolkit/ar/src/main/java/com/arcgismaps/toolkit/ar/WorldScaleSceneView.kt
Outdated
Show resolved
Hide resolved
toolkit/ar/src/main/java/com/arcgismaps/toolkit/ar/WorldScaleSceneView.kt
Outdated
Show resolved
Hide resolved
requestCameraPermissionAutomatically: Boolean, | ||
requestLocationPermissionAutomatically: Boolean, | ||
initializationStatus: MutableState<WorldScaleSceneViewStatus>, | ||
onInitializationStatusChanged: ((WorldScaleSceneViewStatus) -> Unit)? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the thinking behind this (and ultimately MutableState<T>.update
from ARHelpers) being nullable rather than defaulting to {}
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are both meant to be set to the callback parameter that's set on the WorldScaleSceneView, and the convention we've used there for the GeoViews and TableTopSceneView is to make them nullable. IIRC this was because by making them nullable we can easily check if they are set to something meaningful, and if not, we can avoid doing calculation (like listening to the state flows of the underlying geoview). I'm not sure how big an improvement that was but given the convention it makes sense to keep it going. In the case of these utility functions, I think it's easier to read the calling code if it just passes through the value from the WSSV argument, rather than having to do a null check inline
toolkit/ar/src/main/java/com/arcgismaps/toolkit/ar/WorldScaleSceneView.kt
Outdated
Show resolved
Hide resolved
val oldLocationFarFarAway = Location.create(farFarAway, 1.0, 1.0, 0.0, 0.0, true, Instant.now().minusMillis(30000)) | ||
val recentLocationWithNoAccuracy = Location.create(nullIsland, Double.NaN, Double.NaN, 0.0, 0.0, false, Instant.now().minusMillis(50)) | ||
|
||
assert(!shouldUpdateCamera(recentLocationAtNullIsland, cameraAtNullIsland)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better use Truth instead of junit asserts? We have Truth already in the version catalog, just need to add it to the gradle file.
* @since 200.7.0 | ||
*/ | ||
@Composable | ||
private fun TrackLocationWithLocationDataSource( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on naming conventions, since this composable function doesn't return anything, it should be pascal cased and a noun. Also could make the name a bit shorter? How about:
private fun TrackLocationWithLocationDataSource( | |
private fun LocationTracker( |
Camera( | ||
location.position.y, | ||
location.position.x, | ||
if (location.position.hasZ) location.position.z!! else 1.0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better set z to 0.0 if we don't have a z value? This is what Swift is doing.
if (location.position.hasZ) location.position.z!! else 1.0, | |
if (location.position.hasZ) location.position.z!! else 0.0, |
|
||
// We have to do this or the error gets bigger and bigger. | ||
cameraController.transformationMatrix = | ||
TransformationMatrix.createIdentityMatrix() |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
): Boolean { | ||
// filter out old locations | ||
if (Instant.now() | ||
.toEpochMilli() - location.timestamp.toEpochMilli() > 10.0.seconds.inWholeMilliseconds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep thresholds centralized. How about some internal object data WorldScaleParameters
class?
* @since 200.7.0 | ||
*/ | ||
@Composable | ||
private fun requestPermissionsOrFail( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this function returns a remembered state variable, I think we should prefix it with "remember", based on convention. How about:
private fun requestPermissionsOrFail( | |
private fun rememberPermissionsGranted( |
modifier: Modifier = Modifier, | ||
onInitializationStatusChanged: ((WorldScaleSceneViewStatus) -> Unit)? = null, | ||
requestCameraPermissionAutomatically: Boolean = true, | ||
requestLocationPermissionAutomatically: Boolean = true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should have just a single requestPermissionsAutomatically
parameter? What's the use case for having separate ones? We can solve this separate from this PR though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's discuss this. I think there may be use cases for having them separate, but I don't know if it's critical
launch { | ||
locationDataSource.status.collect { | ||
var isLocationDataSourceStarted by remember { mutableStateOf(false) } | ||
if (hasCompletedPermissionRequest && context.checkPermissionsGranted()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When hasCompletedPermissionRequest
is true, doesn't this mean that all required permissions have been granted? Why do we need to then check again with checkPermissionsGranted()
?
scope.launch { | ||
locationDataSource.stop() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the wrapper is destroyed, we should just cancel the scope:
scope.launch { | |
locationDataSource.stop() | |
} | |
scope.cancel() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I put this inside the launch, since otherwise I think we defeat the purpose of the wrapper. Let's discuss.
} | ||
} | ||
|
||
fun startLocationDataSource() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could do this instead in onResume
?
fun startLocationDataSource() { | |
override fun onResume(owner: LifecycleOwner) { |
|
||
/** | ||
* Checks the permissions required for the [WorldScaleSceneView] to function and requests them if necessary. | ||
* If [requestCameraPermissionAutomatically] or [requestLocationPermissionAutomatically] are set to false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doc is out of date
ActivityResultContracts.RequestMultiplePermissions() | ||
) { grantedState: Map<String, Boolean> -> | ||
// This callback will only be relevant to the permissions that were added to the request. | ||
// If the user has set [requestCameraPermissionAutomatically] to false, for example, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is out of date
onInitializationStatusChanged: ((WorldScaleSceneViewStatus) -> Unit)? | ||
): State<Boolean> { | ||
val allPermissionsGranted = remember { mutableStateOf(false) } | ||
val permissionsToRequest = mutableListOf<String>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does this list get populated?
*/ | ||
@Composable | ||
private fun LocationTracker( | ||
locationDataSource: LocationDataSource, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove this parameter and keep the LDS as an internal remembered variable of this function. The LDS is not used y the client code.
* @since 200.7.0 | ||
*/ | ||
internal class LocationDataSourceWrapper(val locationDataSource: LocationDataSource) : DefaultLifecycleObserver { | ||
// This ensures that we can stop the location data source on a scope that won't be cancelled when the composition is destroyed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// This ensures that we can stop the location data source on a scope that won't be cancelled when the composition is destroyed | |
// coroutine scope that is tied to the lifecycle of this LocationDataSourceWrapper |
wrapper.onDestroy(lifecycleOwner) | ||
lifecycleOwner.lifecycle.removeObserver(wrapper) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it' safer to do it in the opposite order:
wrapper.onDestroy(lifecycleOwner) | |
lifecycleOwner.lifecycle.removeObserver(wrapper) | |
lifecycleOwner.lifecycle.removeObserver(wrapper) | |
wrapper.onDestroy(lifecycleOwner) |
… to session starting
} | ||
|
||
return allPermissionsGranted | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing newline
import android.content.Context | ||
import android.content.pm.PackageManager | ||
import androidx.activity.compose.rememberLauncherForActivityResult | ||
import androidx.activity.result.contract.ActivityResultContracts | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.DisposableEffect |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import
import com.arcgismaps.geometry.SpatialReference | ||
import com.arcgismaps.location.Location | ||
import com.arcgismaps.location.LocationDataSource |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import
} | ||
|
||
/** | ||
* Starts the [locationDataSource] and periodically updates the [cameraController] with new locations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
locationDataSource
parameter has been removed...
* Starts the [locationDataSource] and periodically updates the [cameraController] with new locations | |
* Starts a SystemLocationDataSource and periodically updates the [cameraController] with new locations |
val lifecycleOwner = LocalLifecycleOwner.current | ||
DisposableEffect(Unit) { | ||
// This will start the LocationDataSource | ||
val wrapper = LocationDataSourceWrapper(locationDataSource) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This LocationDataSourceWrapper needs to be kept in a remember, otherwise it could be garbage collected. See related comment below on rememberSystemLocationDataSource
.
} | ||
|
||
@Composable | ||
private fun rememberSystemLocationDataSource(): SystemLocationDataSource { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could have this return the LocationDataSourceWrapper
and also do the DisposableEffect
in this remember function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hud10837 - looks good. There are some refinements we can do regarding initialization. For example I noticed that the SceneView may render the globe at full extent (as if looking at it from space) on startup, before we have the initial location. This is something we should be able to avoid. But we can refine that in a separate PR.
toolkit/ar/src/main/java/com/arcgismaps/toolkit/ar/WorldScaleSceneView.kt
Outdated
Show resolved
Hide resolved
…ceneView.kt Co-authored-by: Gunther Heppner <[email protected]>
Related to issue: https://devtopia.esri.com/runtime/kotlin/issues/5175
Description:
Adds world tracking and fixes the sequence of initialization, including handling multiple permissions, in the WorldScaleSceneView
Summary of changes:
Pre-merge Checklist
https://runtime-kotlin.esri.com/view/all/job/vtest/job/toolkit/298/