-
Notifications
You must be signed in to change notification settings - Fork 339
add in projected permissions and hardware access snippets #821
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,9 +16,12 @@ | |
|
|
||
| package com.example.xr.projected | ||
|
|
||
| import android.Manifest | ||
| import android.content.pm.PackageManager | ||
| import android.os.Bundle | ||
| import androidx.activity.ComponentActivity | ||
| import androidx.activity.compose.setContent | ||
| import androidx.activity.result.ActivityResultLauncher | ||
| import androidx.compose.foundation.layout.Box | ||
| import androidx.compose.foundation.layout.fillMaxSize | ||
| import androidx.compose.runtime.Composable | ||
|
|
@@ -27,6 +30,7 @@ import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.setValue | ||
| import androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.core.content.ContextCompat | ||
| import androidx.lifecycle.DefaultLifecycleObserver | ||
| import androidx.lifecycle.LifecycleOwner | ||
| import androidx.lifecycle.lifecycleScope | ||
|
|
@@ -39,6 +43,8 @@ import androidx.xr.projected.ProjectedDisplayController | |
| import androidx.xr.projected.ProjectedDeviceController | ||
| import androidx.xr.projected.ProjectedDeviceController.Capability.Companion.CAPABILITY_VISUAL_UI | ||
| import androidx.xr.projected.experimental.ExperimentalProjectedApi | ||
| import androidx.xr.projected.permissions.ProjectedPermissionsRequestParams | ||
| import androidx.xr.projected.permissions.ProjectedPermissionsResultContract | ||
| import kotlinx.coroutines.launch | ||
|
|
||
| // [START androidxr_projected_ai_glasses_activity] | ||
|
|
@@ -49,6 +55,19 @@ class GlassesMainActivity : ComponentActivity() { | |
| private var isVisualUiSupported by mutableStateOf(false) | ||
| private var areVisualsOn by mutableStateOf(true) | ||
|
|
||
| // [START androidxr_projected_permissions_launcher] | ||
| // Register the permissions launcher using the ProjectedPermissionsResultContract. | ||
| private val requestPermissionLauncher: ActivityResultLauncher<List<ProjectedPermissionsRequestParams>> = | ||
| registerForActivityResult(ProjectedPermissionsResultContract()) { results -> | ||
| if (results[Manifest.permission.CAMERA] == true) { | ||
| // Permission granted, initialize the session/features. | ||
| initializeGlassesFeatures() | ||
| } else { | ||
| // Handle permission denial. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| } | ||
| // [END androidxr_projected_permissions_launcher] | ||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
|
|
||
|
|
@@ -59,6 +78,26 @@ class GlassesMainActivity : ComponentActivity() { | |
| } | ||
| }) | ||
|
|
||
| // [START androidxr_projected_permissions_check_and_request] | ||
| if (hasCameraPermission()) { | ||
| initializeGlassesFeatures() | ||
| } else { | ||
| requestHardwarePermissions() | ||
| } | ||
| // [END androidxr_projected_permissions_check_and_request] | ||
|
|
||
| setContent { | ||
| GlimmerTheme { | ||
| HomeScreen( | ||
| areVisualsOn = areVisualsOn, | ||
| isVisualUiSupported = isVisualUiSupported, | ||
| onClose = { finish() } | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun initializeGlassesFeatures() { | ||
| lifecycleScope.launch { | ||
| // [START androidxr_projected_device_capabilities_check] | ||
| // Check device capabilities | ||
|
|
@@ -75,17 +114,24 @@ class GlassesMainActivity : ComponentActivity() { | |
| ) | ||
| lifecycle.addObserver(observer) | ||
| } | ||
| } | ||
|
|
||
| setContent { | ||
| GlimmerTheme { | ||
| HomeScreen( | ||
| areVisualsOn = areVisualsOn, | ||
| isVisualUiSupported = isVisualUiSupported, | ||
| onClose = { finish() } | ||
| ) | ||
| } | ||
| } | ||
| // [START androidxr_projected_permissions_has_check] | ||
| private fun hasCameraPermission(): Boolean { | ||
| return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == | ||
| PackageManager.PERMISSION_GRANTED | ||
| } | ||
| // [END androidxr_projected_permissions_has_check] | ||
|
|
||
| // [START androidxr_projected_permissions_request] | ||
| private fun requestHardwarePermissions() { | ||
| val params = ProjectedPermissionsRequestParams( | ||
| permissions = listOf(Manifest.permission.CAMERA), | ||
| rationale = "Camera access is required to overlay digital content on your physical environment." | ||
| ) | ||
| requestPermissionLauncher.launch(listOf(params)) | ||
| } | ||
| // [END androidxr_projected_permissions_request] | ||
| } | ||
| // [END androidxr_projected_ai_glasses_activity] | ||
|
|
||
|
|
@@ -123,4 +169,4 @@ fun HomeScreen( | |
| } | ||
| } | ||
| } | ||
| // [END androidxr_projected_ai_glasses_activity_homescreen] | ||
| // [END androidxr_projected_ai_glasses_activity_homescreen] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| /* | ||
| * Copyright 2025 The Android Open Source Project | ||
| * | ||
| * 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 | ||
| * | ||
| * https://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 com.example.xr.projected | ||
|
|
||
| import android.content.Context | ||
| import android.hardware.camera2.CameraCharacteristics | ||
| import android.hardware.camera2.CaptureRequest | ||
| import android.util.Log | ||
| import android.util.Range | ||
| import android.util.Size | ||
| import androidx.activity.ComponentActivity | ||
| import androidx.camera.camera2.interop.Camera2CameraInfo | ||
| import androidx.camera.camera2.interop.CaptureRequestOptions | ||
| import androidx.camera.camera2.interop.ExperimentalCamera2Interop | ||
| import androidx.camera.core.CameraSelector | ||
| import androidx.camera.core.ImageCapture | ||
| import androidx.camera.core.resolutionselector.ResolutionSelector | ||
| import androidx.camera.core.resolutionselector.ResolutionStrategy | ||
| import androidx.camera.lifecycle.ProcessCameraProvider | ||
| import androidx.core.content.ContextCompat | ||
| import androidx.xr.projected.ProjectedContext | ||
| import androidx.xr.projected.experimental.ExperimentalProjectedApi | ||
|
|
||
| private const val TAG = "ProjectedHardware" | ||
|
|
||
| /** | ||
| * Demonstrates how to obtain a context for the projected device (AI glasses) | ||
| * from the host device (phone). | ||
| */ | ||
| // [START androidxr_projected_context_get_projected] | ||
| @OptIn(ExperimentalProjectedApi::class) | ||
| private fun getGlassesContext(context: Context): Context? { | ||
| return try { | ||
| // From a phone Activity or Service, get a context for the AI glasses. | ||
| ProjectedContext.createProjectedDeviceContext(context) | ||
| } catch (e: IllegalStateException) { | ||
| Log.e(TAG, "Failed to create projected device context", e) | ||
| null | ||
trambui09 marked this conversation as resolved.
Show resolved
Hide resolved
trambui09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| // [END androidxr_projected_context_get_projected] | ||
|
|
||
| /** | ||
| * Demonstrates how to obtain a context for the host device (phone) | ||
| * from the projected device (AI glasses). | ||
| */ | ||
| // [START androidxr_projected_context_get_host] | ||
| @OptIn(ExperimentalProjectedApi::class) | ||
| private fun getPhoneContext(activity: ComponentActivity): Context? { | ||
| return try { | ||
| // From an AI glasses Activity, get a context for the phone. | ||
| ProjectedContext.createHostDeviceContext(activity) | ||
| } catch (e: IllegalStateException) { | ||
| Log.e(TAG, "Failed to create host device context", e) | ||
| null | ||
| } | ||
| } | ||
| // [END androidxr_projected_context_get_host] | ||
|
|
||
| /** | ||
| * Demonstrates how to capture an image using the AI glasses' camera. | ||
| */ | ||
| @androidx.annotation.OptIn(ExperimentalCamera2Interop::class) | ||
| @OptIn(ExperimentalProjectedApi::class) | ||
| // [START androidxr_projected_camera_capture] | ||
| private fun startCameraOnGlasses(activity: ComponentActivity) { | ||
| // 1. Get the CameraProvider using the projected context. | ||
| // When using the projected context, DEFAULT_BACK_CAMERA maps to the AI glasses' camera. | ||
| val projectedContext = try { | ||
| ProjectedContext.createProjectedDeviceContext(activity) | ||
| } catch (e: IllegalStateException) { | ||
| Log.e(TAG, "AI Glasses context could not be created", e) | ||
| return | ||
| } | ||
|
|
||
| val cameraProviderFuture = ProcessCameraProvider.getInstance(projectedContext) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just wanted to make sure we're handling the null case in the code here. If projectedContext is null, will this always return a non-null object? could cameraProviderFuture be null?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. createProjectedDeviceContext() throws an IllegalStateException if the projected device isn't found. I'll edit the snippets to catch this. I don't think cameraProviderFuture can be null. |
||
|
|
||
| cameraProviderFuture.addListener({ | ||
| val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() | ||
| val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA | ||
|
|
||
| // 2. Check for the presence of a camera. | ||
| if (!cameraProvider.hasCamera(cameraSelector)) { | ||
| Log.w(TAG, "The selected camera is not available.") | ||
| return@addListener | ||
| } | ||
|
|
||
| // 3. Query supported streaming resolutions using Camera2 Interop. | ||
| val cameraInfo = cameraProvider.getCameraInfo(cameraSelector) | ||
| val camera2CameraInfo = Camera2CameraInfo.from(cameraInfo) | ||
| val cameraCharacteristics = camera2CameraInfo.getCameraCharacteristic( | ||
| CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP | ||
| ) | ||
|
Comment on lines
+105
to
+107
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| // 4. Define the resolution strategy. | ||
| val targetResolution = Size(1920, 1080) | ||
| val resolutionStrategy = ResolutionStrategy( | ||
| targetResolution, | ||
| ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER | ||
| ) | ||
| val resolutionSelector = ResolutionSelector.Builder() | ||
| .setResolutionStrategy(resolutionStrategy) | ||
| .build() | ||
|
|
||
| // 5. If you have other continuous use cases bound, such as Preview or ImageAnalysis, | ||
| // you can use Camera2 Interop's CaptureRequestOptions to set the FPS | ||
| val fpsRange = Range(30, 60) | ||
| val captureRequestOptions = CaptureRequestOptions.Builder() | ||
| .setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange) | ||
| .build() | ||
|
|
||
| // 6. Initialize the ImageCapture use case with options. | ||
| val imageCapture = ImageCapture.Builder() | ||
| // Optional: Configure resolution, format, etc. | ||
| .setResolutionSelector(resolutionSelector) | ||
| .build() | ||
|
|
||
| try { | ||
| // Unbind use cases before rebinding. | ||
| cameraProvider.unbindAll() | ||
|
|
||
| // Bind use cases to camera using the Activity as the LifecycleOwner. | ||
| cameraProvider.bindToLifecycle( | ||
| activity, | ||
| cameraSelector, | ||
| imageCapture | ||
| ) | ||
| } catch (exc: Exception) { | ||
| Log.e(TAG, "Use case binding failed", exc) | ||
| } | ||
| }, ContextCompat.getMainExecutor(activity)) | ||
| } | ||
| // [END androidxr_projected_camera_capture] | ||
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.
Should we use rememberLauncherForActivityResult() instead?
https://developer.android.com/develop/ui/compose/libraries#requesting-runtime-permissions
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 rememberLauncherForActivityResult cannot be used in onCreate; it can only be used inside a @composable function. The registerForActivityResult ensures the launcher is registered before the Activity starts.